This is an automated email from the ASF dual-hosted git repository.

fresh-borzoni pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new dbbaf9b8 [rust] Support API_VERSION in rust client. (#531)
dbbaf9b8 is described below

commit dbbaf9b8c398d019cef3bca1065a83e8d5eaea0a
Author: Hongshun Wang <[email protected]>
AuthorDate: Sat May 9 16:53:13 2026 +0800

    [rust] Support API_VERSION in rust client. (#531)
    
    * [rust] Support API_VERSION in rust client
    
    * remove useless write_version_type and read_version_type which is same as 
java client does.
    
    * Add test about kv_format_v2.
    
    * return apiKey.highestSupportedVersion as default.
    
    * check whether server type mismatch.
    
    * add test about unknown server type
---
 crates/fluss-test-cluster/test-images.env          |   2 +-
 crates/fluss/src/client/lookup/lookup_sender.rs    |   8 +-
 crates/fluss/src/cluster/mod.rs                    |  17 +
 crates/fluss/src/error.rs                          |  12 +
 crates/fluss/src/proto/fluss_api.proto             |  19 +-
 crates/fluss/src/rpc/api_key.rs                    |  45 +++
 .../message/{init_writer.rs => api_versions.rs}    |  38 +--
 crates/fluss/src/rpc/message/authenticate.rs       |  10 +-
 crates/fluss/src/rpc/message/create_database.rs    |  11 +-
 crates/fluss/src/rpc/message/create_partition.rs   |  11 +-
 crates/fluss/src/rpc/message/create_table.rs       |  11 +-
 crates/fluss/src/rpc/message/database_exists.rs    |  10 +-
 crates/fluss/src/rpc/message/drop_database.rs      |  10 +-
 crates/fluss/src/rpc/message/drop_partition.rs     |  11 +-
 crates/fluss/src/rpc/message/drop_table.rs         |  11 +-
 crates/fluss/src/rpc/message/fetch.rs              |  11 +-
 crates/fluss/src/rpc/message/get_database_info.rs  |  10 +-
 .../src/rpc/message/get_latest_lake_snapshot.rs    |  10 +-
 crates/fluss/src/rpc/message/get_security_token.rs |  10 +-
 crates/fluss/src/rpc/message/get_table.rs          |  10 +-
 crates/fluss/src/rpc/message/get_table_schema.rs   |  10 +-
 crates/fluss/src/rpc/message/header.rs             |  10 +-
 crates/fluss/src/rpc/message/init_writer.rs        |  11 +-
 crates/fluss/src/rpc/message/limit_scan.rs         |  11 +-
 crates/fluss/src/rpc/message/list_databases.rs     |  10 +-
 crates/fluss/src/rpc/message/list_offsets.rs       |  13 +-
 .../fluss/src/rpc/message/list_partition_infos.rs  |  11 +-
 crates/fluss/src/rpc/message/list_tables.rs        |  11 +-
 crates/fluss/src/rpc/message/lookup.rs             |  13 +-
 crates/fluss/src/rpc/message/mod.rs                |  31 +-
 crates/fluss/src/rpc/message/prefix_lookup.rs      |  13 +-
 crates/fluss/src/rpc/message/produce_log.rs        |  11 +-
 crates/fluss/src/rpc/message/put_kv.rs             |  11 +-
 crates/fluss/src/rpc/message/table_exists.rs       |  11 +-
 crates/fluss/src/rpc/message/update_metadata.rs    |  11 +-
 crates/fluss/src/rpc/server_connection.rs          | 341 ++++++++++++++++++---
 crates/fluss/tests/integration/kv_table.rs         |  71 +++++
 37 files changed, 611 insertions(+), 266 deletions(-)

diff --git a/crates/fluss-test-cluster/test-images.env 
b/crates/fluss-test-cluster/test-images.env
index 3aa8e735..5cd91417 100644
--- a/crates/fluss-test-cluster/test-images.env
+++ b/crates/fluss-test-cluster/test-images.env
@@ -1,4 +1,4 @@
 FLUSS_IMAGE=apache/fluss
-FLUSS_VERSION=0.9.0-incubating
+FLUSS_VERSION=0.9.1-incubating
 ZOOKEEPER_IMAGE=zookeeper
 ZOOKEEPER_VERSION=3.9.2
diff --git a/crates/fluss/src/client/lookup/lookup_sender.rs 
b/crates/fluss/src/client/lookup/lookup_sender.rs
index efcd6853..1e337f83 100644
--- a/crates/fluss/src/client/lookup/lookup_sender.rs
+++ b/crates/fluss/src/client/lookup/lookup_sender.rs
@@ -22,9 +22,7 @@ use crate::error::{Error, FlussError, Result};
 use crate::metadata::{TableBucket, TablePath};
 use crate::proto::{LookupResponse, PrefixLookupResponse};
 use crate::rpc::ServerConnection;
-use crate::rpc::message::{
-    LookupRequest, PrefixLookupRequest, ReadVersionedType, RequestBody, 
WriteVersionedType,
-};
+use crate::rpc::message::{LookupRequest, PrefixLookupRequest, ReadType, 
RequestBody, WriteType};
 use crate::{BucketId, PartitionId, TableId};
 use bytes::Bytes;
 use futures::stream::{FuturesUnordered, StreamExt};
@@ -51,8 +49,8 @@ struct BucketResponse<V> {
 }
 
 trait LookupProtocol {
-    type Request: RequestBody<ResponseBody = Self::Response> + Send + 
WriteVersionedType<Vec<u8>>;
-    type Response: ReadVersionedType<Cursor<Vec<u8>>> + Send;
+    type Request: RequestBody<ResponseBody = Self::Response> + Send + 
WriteType<Vec<u8>>;
+    type Response: ReadType<Cursor<Vec<u8>>> + Send;
     type Value: Send;
 
     const OP_NAME: &'static str;
diff --git a/crates/fluss/src/cluster/mod.rs b/crates/fluss/src/cluster/mod.rs
index 8b825eee..863f8ed5 100644
--- a/crates/fluss/src/cluster/mod.rs
+++ b/crates/fluss/src/cluster/mod.rs
@@ -79,6 +79,23 @@ pub enum ServerType {
     CoordinatorServer,
 }
 
+impl ServerType {
+    pub fn to_type_id(&self) -> i32 {
+        match self {
+            ServerType::CoordinatorServer => 1,
+            ServerType::TabletServer => 2,
+        }
+    }
+
+    pub fn from_type_id(type_id: i32) -> Option<ServerType> {
+        match type_id {
+            1 => Some(ServerType::CoordinatorServer),
+            2 => Some(ServerType::TabletServer),
+            _ => None,
+        }
+    }
+}
+
 impl fmt::Display for ServerType {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
diff --git a/crates/fluss/src/error.rs b/crates/fluss/src/error.rs
index c2f72a74..4bd0690e 100644
--- a/crates/fluss/src/error.rs
+++ b/crates/fluss/src/error.rs
@@ -115,6 +115,18 @@ pub enum Error {
 
     #[snafu(visibility(pub(crate)), display("Fluss API Error: {}.", 
api_error))]
     FlussAPIError { api_error: ApiError },
+
+    #[snafu(
+        visibility(pub(crate)),
+        display("Unsupported API version: {}.", message)
+    )]
+    UnsupportedVersion { message: String },
+
+    /// The server advertised a `server_type` that does not match the one 
expected
+    /// for the target `ServerNode` (e.g. connecting to a coordinator on a 
tablet
+    /// server address).
+    #[snafu(visibility(pub(crate)), display("Invalid server type: {}.", 
message))]
+    InvalidServerType { message: String },
 }
 
 /// Convenience constructors for API errors that may be raised client-side.
diff --git a/crates/fluss/src/proto/fluss_api.proto 
b/crates/fluss/src/proto/fluss_api.proto
index a544906b..2add80d7 100644
--- a/crates/fluss/src/proto/fluss_api.proto
+++ b/crates/fluss/src/proto/fluss_api.proto
@@ -24,6 +24,23 @@ message ErrorResponse {
   optional string error_message = 2;
 }
 
+// api versions request and response
+message ApiVersionsRequest {
+  required string client_software_name = 1;
+  required string client_software_version = 2;
+}
+
+message ApiVersionsResponse {
+  repeated PbApiVersion api_versions = 1;
+  optional int32 server_type = 2;
+}
+
+message PbApiVersion {
+  required int32 api_key = 1;
+  required int32 min_version = 2;
+  required int32 max_version = 3;
+}
+
 // metadata request and response, request send from client to each server.
 message MetadataRequest {
   repeated PbTablePath table_path = 1;
@@ -483,4 +500,4 @@ message InitWriterRequest {
 
 message InitWriterResponse {
   required int64 writer_id = 1;
-}
\ No newline at end of file
+}
diff --git a/crates/fluss/src/rpc/api_key.rs b/crates/fluss/src/rpc/api_key.rs
index 1ea0269d..977b69d1 100644
--- a/crates/fluss/src/rpc/api_key.rs
+++ b/crates/fluss/src/rpc/api_key.rs
@@ -16,9 +16,11 @@
 // under the License.
 
 use crate::rpc::api_key::ApiKey::Unknown;
+use crate::rpc::api_version::{ApiVersion, ApiVersionRange};
 
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
 pub enum ApiKey {
+    ApiVersion,                 // 1000
     CreateDatabase,             // 1001
     DropDatabase,               // 1002
     ListDatabases,              // 1003
@@ -48,9 +50,50 @@ pub enum ApiKey {
     Unknown(i16),
 }
 
+impl ApiKey {
+    /// Returns the range of versions supported by the client for this API key.
+    pub fn supported_versions(&self) -> Option<ApiVersionRange> {
+        match self {
+            // Most APIs only support v0.
+            ApiKey::ApiVersion
+            | ApiKey::CreateDatabase
+            | ApiKey::DropDatabase
+            | ApiKey::ListDatabases
+            | ApiKey::DatabaseExists
+            | ApiKey::CreateTable
+            | ApiKey::DropTable
+            | ApiKey::GetTable
+            | ApiKey::ListTables
+            | ApiKey::ListPartitionInfos
+            | ApiKey::TableExists
+            | ApiKey::GetTableSchema
+            | ApiKey::MetaData
+            | ApiKey::ProduceLog
+            | ApiKey::FetchLog
+            | ApiKey::ListOffsets
+            | ApiKey::GetFileSystemSecurityToken
+            | ApiKey::InitWriter
+            | ApiKey::GetLatestLakeSnapshot
+            | ApiKey::LimitScan
+            | ApiKey::GetDatabaseInfo
+            | ApiKey::CreatePartition
+            | ApiKey::DropPartition
+            | ApiKey::Authenticate
+            // TODO(key-encoding-v1): The Java server supports v0..v1 for these
+            // APIs, but the Rust client has not yet implemented the v1 key
+            // encoding format. Pinned to v0 until that is done.
+            | ApiKey::PutKv | ApiKey::Lookup | ApiKey::PrefixLookup => {
+                Some(ApiVersionRange::new(ApiVersion(0), ApiVersion(0)))
+            }
+            Unknown(_) => None,
+        }
+    }
+}
+
 impl From<i16> for ApiKey {
     fn from(key: i16) -> Self {
         match key {
+            1000 => ApiKey::ApiVersion,
             1001 => ApiKey::CreateDatabase,
             1002 => ApiKey::DropDatabase,
             1003 => ApiKey::ListDatabases,
@@ -86,6 +129,7 @@ impl From<i16> for ApiKey {
 impl From<ApiKey> for i16 {
     fn from(key: ApiKey) -> Self {
         match key {
+            ApiKey::ApiVersion => 1000,
             ApiKey::CreateDatabase => 1001,
             ApiKey::DropDatabase => 1002,
             ApiKey::ListDatabases => 1003,
@@ -124,6 +168,7 @@ mod tests {
     #[test]
     fn api_key_round_trip() {
         let cases = [
+            (1000, ApiKey::ApiVersion),
             (1001, ApiKey::CreateDatabase),
             (1002, ApiKey::DropDatabase),
             (1003, ApiKey::ListDatabases),
diff --git a/crates/fluss/src/rpc/message/init_writer.rs 
b/crates/fluss/src/rpc/message/api_versions.rs
similarity index 53%
copy from crates/fluss/src/rpc/message/init_writer.rs
copy to crates/fluss/src/rpc/message/api_versions.rs
index 0bbb0dc5..579c66a7 100644
--- a/crates/fluss/src/rpc/message/init_writer.rs
+++ b/crates/fluss/src/rpc/message/api_versions.rs
@@ -15,36 +15,36 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use crate::proto::{InitWriterResponse, PbTablePath};
+use crate::proto::{
+    ApiVersionsRequest as ProtoApiVersionsRequest, ApiVersionsResponse as 
ProtoApiVersionsResponse,
+};
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::{ReadError, WriteError};
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
+use crate::{impl_read_type, impl_write_type};
 use bytes::{Buf, BufMut};
 use prost::Message;
 
-pub struct InitWriterRequest {
-    pub inner_request: proto::InitWriterRequest,
+#[derive(Debug, Clone)]
+pub struct ApiVersionsRequest {
+    pub inner_request: ProtoApiVersionsRequest,
 }
 
-impl InitWriterRequest {
-    pub fn new(table_paths: Vec<PbTablePath>) -> Self {
-        InitWriterRequest {
-            inner_request: proto::InitWriterRequest {
-                table_path: table_paths,
+impl ApiVersionsRequest {
+    pub fn new(client_name: &str, client_version: &str) -> Self {
+        Self {
+            inner_request: ProtoApiVersionsRequest {
+                client_software_name: client_name.to_string(),
+                client_software_version: client_version.to_string(),
             },
         }
     }
 }
 
-impl RequestBody for InitWriterRequest {
-    type ResponseBody = InitWriterResponse;
-
-    const API_KEY: ApiKey = ApiKey::InitWriter;
-
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
+impl RequestBody for ApiVersionsRequest {
+    type ResponseBody = ProtoApiVersionsResponse;
+    const API_KEY: ApiKey = ApiKey::ApiVersion;
 }
 
-impl_write_version_type!(InitWriterRequest);
-impl_read_version_type!(InitWriterResponse);
+impl_write_type!(ApiVersionsRequest);
+impl_read_type!(ProtoApiVersionsResponse);
diff --git a/crates/fluss/src/rpc/message/authenticate.rs 
b/crates/fluss/src/rpc/message/authenticate.rs
index 1292cdc9..1874b304 100644
--- a/crates/fluss/src/rpc/message/authenticate.rs
+++ b/crates/fluss/src/rpc/message/authenticate.rs
@@ -17,10 +17,9 @@
 
 use crate::proto::{AuthenticateRequest as ProtoAuthenticateRequest, 
AuthenticateResponse};
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::{ReadError, WriteError};
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
-use crate::{impl_read_version_type, impl_write_version_type};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
+use crate::{impl_read_type, impl_write_type};
 use bytes::{Buf, BufMut};
 use prost::Message;
 
@@ -61,11 +60,10 @@ impl AuthenticateRequest {
 impl RequestBody for AuthenticateRequest {
     type ResponseBody = AuthenticateResponse;
     const API_KEY: ApiKey = ApiKey::Authenticate;
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(AuthenticateRequest);
-impl_read_version_type!(AuthenticateResponse);
+impl_write_type!(AuthenticateRequest);
+impl_read_type!(AuthenticateResponse);
 
 #[cfg(test)]
 mod tests {
diff --git a/crates/fluss/src/rpc/message/create_database.rs 
b/crates/fluss/src/rpc/message/create_database.rs
index e03cd1ff..ed0868da 100644
--- a/crates/fluss/src/rpc/message/create_database.rs
+++ b/crates/fluss/src/rpc/message/create_database.rs
@@ -16,15 +16,14 @@
 // under the License.
 
 use crate::metadata::DatabaseDescriptor;
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::{impl_read_type, impl_write_type, proto};
 
 use crate::error::Result as FlussResult;
 use crate::proto::CreateDatabaseResponse;
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::ReadError;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
 
 use bytes::{Buf, BufMut};
 use prost::Message;
@@ -60,9 +59,7 @@ impl RequestBody for CreateDatabaseRequest {
     type ResponseBody = CreateDatabaseResponse;
 
     const API_KEY: ApiKey = ApiKey::CreateDatabase;
-
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(CreateDatabaseRequest);
-impl_read_version_type!(CreateDatabaseResponse);
+impl_write_type!(CreateDatabaseRequest);
+impl_read_type!(CreateDatabaseResponse);
diff --git a/crates/fluss/src/rpc/message/create_partition.rs 
b/crates/fluss/src/rpc/message/create_partition.rs
index 93dbf70d..ad633655 100644
--- a/crates/fluss/src/rpc/message/create_partition.rs
+++ b/crates/fluss/src/rpc/message/create_partition.rs
@@ -18,11 +18,10 @@
 use crate::metadata::{PartitionSpec, TablePath};
 use crate::proto::CreatePartitionResponse;
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::convert::to_table_path;
 use crate::rpc::frame::{ReadError, WriteError};
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
+use crate::{impl_read_type, impl_write_type, proto};
 use bytes::{Buf, BufMut};
 use prost::Message;
 
@@ -51,9 +50,7 @@ impl RequestBody for CreatePartitionRequest {
     type ResponseBody = CreatePartitionResponse;
 
     const API_KEY: ApiKey = ApiKey::CreatePartition;
-
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(CreatePartitionRequest);
-impl_read_version_type!(CreatePartitionResponse);
+impl_write_type!(CreatePartitionRequest);
+impl_read_type!(CreatePartitionResponse);
diff --git a/crates/fluss/src/rpc/message/create_table.rs 
b/crates/fluss/src/rpc/message/create_table.rs
index 69865b89..4647fec6 100644
--- a/crates/fluss/src/rpc/message/create_table.rs
+++ b/crates/fluss/src/rpc/message/create_table.rs
@@ -16,16 +16,15 @@
 // under the License.
 
 use crate::metadata::{JsonSerde, TableDescriptor, TablePath};
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::{impl_read_type, impl_write_type, proto};
 
 use crate::error::Result as FlussResult;
 use crate::proto::CreateTableResponse;
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::convert::to_table_path;
 use crate::rpc::frame::ReadError;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
 
 use bytes::{Buf, BufMut};
 use prost::Message;
@@ -55,9 +54,7 @@ impl RequestBody for CreateTableRequest {
     type ResponseBody = CreateTableResponse;
 
     const API_KEY: ApiKey = ApiKey::CreateTable;
-
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(CreateTableRequest);
-impl_read_version_type!(CreateTableResponse);
+impl_write_type!(CreateTableRequest);
+impl_read_type!(CreateTableResponse);
diff --git a/crates/fluss/src/rpc/message/database_exists.rs 
b/crates/fluss/src/rpc/message/database_exists.rs
index 7e717a4e..4a9588a2 100644
--- a/crates/fluss/src/rpc/message/database_exists.rs
+++ b/crates/fluss/src/rpc/message/database_exists.rs
@@ -18,10 +18,9 @@
 use crate::rpc::frame::ReadError;
 
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
+use crate::{impl_read_type, impl_write_type, proto};
 use bytes::{Buf, BufMut};
 use prost::Message;
 
@@ -44,8 +43,7 @@ impl RequestBody for DatabaseExistsRequest {
     type ResponseBody = proto::DatabaseExistsResponse;
 
     const API_KEY: ApiKey = ApiKey::DatabaseExists;
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(DatabaseExistsRequest);
-impl_read_version_type!(proto::DatabaseExistsResponse);
+impl_write_type!(DatabaseExistsRequest);
+impl_read_type!(proto::DatabaseExistsResponse);
diff --git a/crates/fluss/src/rpc/message/drop_database.rs 
b/crates/fluss/src/rpc/message/drop_database.rs
index 663e970a..bf7477f3 100644
--- a/crates/fluss/src/rpc/message/drop_database.rs
+++ b/crates/fluss/src/rpc/message/drop_database.rs
@@ -18,10 +18,9 @@
 use crate::rpc::frame::ReadError;
 
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
+use crate::{impl_read_type, impl_write_type, proto};
 use bytes::{Buf, BufMut};
 use prost::Message;
 
@@ -46,8 +45,7 @@ impl RequestBody for DropDatabaseRequest {
     type ResponseBody = proto::DropDatabaseResponse;
 
     const API_KEY: ApiKey = ApiKey::DropDatabase;
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(DropDatabaseRequest);
-impl_read_version_type!(proto::DropDatabaseResponse);
+impl_write_type!(DropDatabaseRequest);
+impl_read_type!(proto::DropDatabaseResponse);
diff --git a/crates/fluss/src/rpc/message/drop_partition.rs 
b/crates/fluss/src/rpc/message/drop_partition.rs
index ddc97d83..c7494acb 100644
--- a/crates/fluss/src/rpc/message/drop_partition.rs
+++ b/crates/fluss/src/rpc/message/drop_partition.rs
@@ -18,11 +18,10 @@
 use crate::metadata::{PartitionSpec, TablePath};
 use crate::proto::DropPartitionResponse;
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::convert::to_table_path;
 use crate::rpc::frame::{ReadError, WriteError};
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
+use crate::{impl_read_type, impl_write_type, proto};
 use bytes::{Buf, BufMut};
 use prost::Message;
 
@@ -51,9 +50,7 @@ impl RequestBody for DropPartitionRequest {
     type ResponseBody = DropPartitionResponse;
 
     const API_KEY: ApiKey = ApiKey::DropPartition;
-
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(DropPartitionRequest);
-impl_read_version_type!(DropPartitionResponse);
+impl_write_type!(DropPartitionRequest);
+impl_read_type!(DropPartitionResponse);
diff --git a/crates/fluss/src/rpc/message/drop_table.rs 
b/crates/fluss/src/rpc/message/drop_table.rs
index a2b3f2d1..b452cf07 100644
--- a/crates/fluss/src/rpc/message/drop_table.rs
+++ b/crates/fluss/src/rpc/message/drop_table.rs
@@ -16,16 +16,15 @@
 // under the License.
 
 use crate::metadata::TablePath;
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::{impl_read_type, impl_write_type, proto};
 
 use crate::proto::DropTableResponse;
 use crate::rpc::frame::ReadError;
 
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::convert::to_table_path;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
 
 use bytes::{Buf, BufMut};
 use prost::Message;
@@ -50,9 +49,7 @@ impl RequestBody for DropTableRequest {
     type ResponseBody = DropTableResponse;
 
     const API_KEY: ApiKey = ApiKey::DropTable;
-
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(DropTableRequest);
-impl_read_version_type!(DropTableResponse);
+impl_write_type!(DropTableRequest);
+impl_read_type!(DropTableResponse);
diff --git a/crates/fluss/src/rpc/message/fetch.rs 
b/crates/fluss/src/rpc/message/fetch.rs
index 15876069..67930f84 100644
--- a/crates/fluss/src/rpc/message/fetch.rs
+++ b/crates/fluss/src/rpc/message/fetch.rs
@@ -19,10 +19,9 @@ use crate::proto::FetchLogResponse;
 use crate::rpc::frame::ReadError;
 
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
+use crate::{impl_read_type, impl_write_type, proto};
 use prost::Message;
 
 use bytes::{Buf, BufMut};
@@ -50,9 +49,7 @@ impl RequestBody for FetchLogRequest {
     type ResponseBody = FetchLogResponse;
 
     const API_KEY: ApiKey = ApiKey::FetchLog;
-
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(FetchLogRequest);
-impl_read_version_type!(FetchLogResponse);
+impl_write_type!(FetchLogRequest);
+impl_read_type!(FetchLogResponse);
diff --git a/crates/fluss/src/rpc/message/get_database_info.rs 
b/crates/fluss/src/rpc/message/get_database_info.rs
index 6468bebd..63647d52 100644
--- a/crates/fluss/src/rpc/message/get_database_info.rs
+++ b/crates/fluss/src/rpc/message/get_database_info.rs
@@ -18,10 +18,9 @@
 use crate::rpc::frame::ReadError;
 
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
+use crate::{impl_read_type, impl_write_type, proto};
 use bytes::{Buf, BufMut};
 use prost::Message;
 
@@ -44,8 +43,7 @@ impl RequestBody for GetDatabaseInfoRequest {
     type ResponseBody = proto::GetDatabaseInfoResponse;
 
     const API_KEY: ApiKey = ApiKey::GetDatabaseInfo;
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(GetDatabaseInfoRequest);
-impl_read_version_type!(proto::GetDatabaseInfoResponse);
+impl_write_type!(GetDatabaseInfoRequest);
+impl_read_type!(proto::GetDatabaseInfoResponse);
diff --git a/crates/fluss/src/rpc/message/get_latest_lake_snapshot.rs 
b/crates/fluss/src/rpc/message/get_latest_lake_snapshot.rs
index a632a159..0b59384d 100644
--- a/crates/fluss/src/rpc/message/get_latest_lake_snapshot.rs
+++ b/crates/fluss/src/rpc/message/get_latest_lake_snapshot.rs
@@ -18,14 +18,13 @@
 use crate::proto;
 use crate::proto::PbTablePath;
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
 
 use crate::metadata::TablePath;
 use crate::rpc::frame::ReadError;
 
-use crate::{impl_read_version_type, impl_write_version_type};
+use crate::{impl_read_type, impl_write_type};
 use bytes::{Buf, BufMut};
 use prost::Message;
 
@@ -50,8 +49,7 @@ impl GetLatestLakeSnapshotRequest {
 impl RequestBody for GetLatestLakeSnapshotRequest {
     type ResponseBody = proto::GetLatestLakeSnapshotResponse;
     const API_KEY: ApiKey = ApiKey::GetLatestLakeSnapshot;
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(GetLatestLakeSnapshotRequest);
-impl_read_version_type!(proto::GetLatestLakeSnapshotResponse);
+impl_write_type!(GetLatestLakeSnapshotRequest);
+impl_read_type!(proto::GetLatestLakeSnapshotResponse);
diff --git a/crates/fluss/src/rpc/message/get_security_token.rs 
b/crates/fluss/src/rpc/message/get_security_token.rs
index 7995232d..741c8482 100644
--- a/crates/fluss/src/rpc/message/get_security_token.rs
+++ b/crates/fluss/src/rpc/message/get_security_token.rs
@@ -17,10 +17,9 @@
 
 use crate::proto::{GetFileSystemSecurityTokenRequest, 
GetFileSystemSecurityTokenResponse};
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::{ReadError, WriteError};
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
-use crate::{impl_read_version_type, impl_write_version_type};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
+use crate::{impl_read_type, impl_write_type};
 use bytes::{Buf, BufMut};
 use prost::Message;
 
@@ -46,8 +45,7 @@ impl Default for GetSecurityTokenRequest {
 impl RequestBody for GetSecurityTokenRequest {
     type ResponseBody = GetFileSystemSecurityTokenResponse;
     const API_KEY: ApiKey = ApiKey::GetFileSystemSecurityToken;
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(GetSecurityTokenRequest);
-impl_read_version_type!(GetFileSystemSecurityTokenResponse);
+impl_write_type!(GetSecurityTokenRequest);
+impl_read_type!(GetFileSystemSecurityTokenResponse);
diff --git a/crates/fluss/src/rpc/message/get_table.rs 
b/crates/fluss/src/rpc/message/get_table.rs
index 61657f7a..a7562f92 100644
--- a/crates/fluss/src/rpc/message/get_table.rs
+++ b/crates/fluss/src/rpc/message/get_table.rs
@@ -17,14 +17,13 @@
 
 use crate::proto::{GetTableInfoRequest, GetTableInfoResponse, PbTablePath};
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
 
 use crate::metadata::TablePath;
 use crate::rpc::frame::ReadError;
 
-use crate::{impl_read_version_type, impl_write_version_type};
+use crate::{impl_read_type, impl_write_type};
 use bytes::{Buf, BufMut};
 use prost::Message;
 
@@ -49,8 +48,7 @@ impl GetTableRequest {
 impl RequestBody for GetTableRequest {
     type ResponseBody = GetTableInfoResponse;
     const API_KEY: ApiKey = ApiKey::GetTable;
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(GetTableRequest);
-impl_read_version_type!(GetTableInfoResponse);
+impl_write_type!(GetTableRequest);
+impl_read_type!(GetTableInfoResponse);
diff --git a/crates/fluss/src/rpc/message/get_table_schema.rs 
b/crates/fluss/src/rpc/message/get_table_schema.rs
index ad7b23fb..1c7c00b7 100644
--- a/crates/fluss/src/rpc/message/get_table_schema.rs
+++ b/crates/fluss/src/rpc/message/get_table_schema.rs
@@ -17,14 +17,13 @@
 
 use crate::proto::{GetTableSchemaRequest, GetTableSchemaResponse, PbTablePath};
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
 
 use crate::metadata::TablePath;
 use crate::rpc::frame::ReadError;
 
-use crate::{impl_read_version_type, impl_write_version_type};
+use crate::{impl_read_type, impl_write_type};
 use bytes::{Buf, BufMut};
 use prost::Message;
 
@@ -50,8 +49,7 @@ impl GetTableSchemaRequestMsg {
 impl RequestBody for GetTableSchemaRequestMsg {
     type ResponseBody = GetTableSchemaResponse;
     const API_KEY: ApiKey = ApiKey::GetTableSchema;
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(GetTableSchemaRequestMsg);
-impl_read_version_type!(GetTableSchemaResponse);
+impl_write_type!(GetTableSchemaRequestMsg);
+impl_read_type!(GetTableSchemaResponse);
diff --git a/crates/fluss/src/rpc/message/header.rs 
b/crates/fluss/src/rpc/message/header.rs
index 2f5848aa..11155f68 100644
--- a/crates/fluss/src/rpc/message/header.rs
+++ b/crates/fluss/src/rpc/message/header.rs
@@ -19,7 +19,7 @@ use crate::proto::ErrorResponse;
 use crate::rpc::api_key::ApiKey;
 use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::{ReadError, WriteError};
-use crate::rpc::message::{ReadVersionedType, WriteVersionedType};
+use crate::rpc::message::{ReadType, WriteType};
 use bytes::{Buf, BufMut};
 use prost::Message;
 
@@ -42,11 +42,11 @@ pub struct RequestHeader {
     pub client_id: Option<String>,
 }
 
-impl<W> WriteVersionedType<W> for RequestHeader
+impl<W> WriteType<W> for RequestHeader
 where
     W: BufMut,
 {
-    fn write_versioned(&self, writer: &mut W, _version: ApiVersion) -> 
Result<(), WriteError> {
+    fn write(&self, writer: &mut W) -> Result<(), WriteError> {
         writer.put_i16(self.request_api_key.into());
         writer.put_i16(self.request_api_version.0);
         writer.put_i32(self.request_id);
@@ -60,11 +60,11 @@ pub struct ResponseHeader {
     pub error_response: Option<ErrorResponse>,
 }
 
-impl<R> ReadVersionedType<R> for ResponseHeader
+impl<R> ReadType<R> for ResponseHeader
 where
     R: Buf,
 {
-    fn read_versioned(reader: &mut R, _version: ApiVersion) -> Result<Self, 
ReadError> {
+    fn read(reader: &mut R) -> Result<Self, ReadError> {
         let resp_type = reader.get_u8();
         let request_id = reader.get_i32();
         if resp_type != SUCCESS_RESPONSE {
diff --git a/crates/fluss/src/rpc/message/init_writer.rs 
b/crates/fluss/src/rpc/message/init_writer.rs
index 0bbb0dc5..b2e64a5f 100644
--- a/crates/fluss/src/rpc/message/init_writer.rs
+++ b/crates/fluss/src/rpc/message/init_writer.rs
@@ -17,10 +17,9 @@
 
 use crate::proto::{InitWriterResponse, PbTablePath};
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::{ReadError, WriteError};
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
+use crate::{impl_read_type, impl_write_type, proto};
 use bytes::{Buf, BufMut};
 use prost::Message;
 
@@ -42,9 +41,7 @@ impl RequestBody for InitWriterRequest {
     type ResponseBody = InitWriterResponse;
 
     const API_KEY: ApiKey = ApiKey::InitWriter;
-
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(InitWriterRequest);
-impl_read_version_type!(InitWriterResponse);
+impl_write_type!(InitWriterRequest);
+impl_read_type!(InitWriterResponse);
diff --git a/crates/fluss/src/rpc/message/limit_scan.rs 
b/crates/fluss/src/rpc/message/limit_scan.rs
index d83a2e8b..c71b03c3 100644
--- a/crates/fluss/src/rpc/message/limit_scan.rs
+++ b/crates/fluss/src/rpc/message/limit_scan.rs
@@ -19,10 +19,9 @@ use crate::proto::LimitScanResponse;
 use crate::rpc::frame::ReadError;
 
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
+use crate::{impl_read_type, impl_write_type, proto};
 use prost::Message;
 
 use bytes::{Buf, BufMut};
@@ -50,9 +49,7 @@ impl RequestBody for LimitScanRequest {
     type ResponseBody = LimitScanResponse;
 
     const API_KEY: ApiKey = ApiKey::LimitScan;
-
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(LimitScanRequest);
-impl_read_version_type!(LimitScanResponse);
+impl_write_type!(LimitScanRequest);
+impl_read_type!(LimitScanResponse);
diff --git a/crates/fluss/src/rpc/message/list_databases.rs 
b/crates/fluss/src/rpc/message/list_databases.rs
index 83226ab1..21e16400 100644
--- a/crates/fluss/src/rpc/message/list_databases.rs
+++ b/crates/fluss/src/rpc/message/list_databases.rs
@@ -18,10 +18,9 @@
 use crate::rpc::frame::ReadError;
 
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
+use crate::{impl_read_type, impl_write_type, proto};
 use bytes::{Buf, BufMut};
 use prost::Message;
 
@@ -42,8 +41,7 @@ impl RequestBody for ListDatabasesRequest {
     type ResponseBody = proto::ListDatabasesResponse;
 
     const API_KEY: ApiKey = ApiKey::ListDatabases;
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(ListDatabasesRequest);
-impl_read_version_type!(proto::ListDatabasesResponse);
+impl_write_type!(ListDatabasesRequest);
+impl_read_type!(proto::ListDatabasesResponse);
diff --git a/crates/fluss/src/rpc/message/list_offsets.rs 
b/crates/fluss/src/rpc/message/list_offsets.rs
index 262645a6..2ec14370 100644
--- a/crates/fluss/src/rpc/message/list_offsets.rs
+++ b/crates/fluss/src/rpc/message/list_offsets.rs
@@ -15,9 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use crate::{
-    BucketId, PartitionId, TableId, impl_read_version_type, 
impl_write_version_type, proto,
-};
+use crate::{BucketId, PartitionId, TableId, impl_read_type, impl_write_type, 
proto};
 
 use crate::error::Result as FlussResult;
 use crate::error::{Error, FlussError};
@@ -25,9 +23,8 @@ use crate::proto::{ErrorResponse, ListOffsetsResponse};
 use crate::rpc::frame::ReadError;
 
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
 use std::collections::HashMap;
 
 use bytes::{Buf, BufMut};
@@ -98,12 +95,10 @@ impl RequestBody for ListOffsetsRequest {
     type ResponseBody = ListOffsetsResponse;
 
     const API_KEY: ApiKey = ApiKey::ListOffsets;
-
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(ListOffsetsRequest);
-impl_read_version_type!(ListOffsetsResponse);
+impl_write_type!(ListOffsetsRequest);
+impl_read_type!(ListOffsetsResponse);
 
 impl ListOffsetsResponse {
     pub fn offsets(&self) -> FlussResult<HashMap<i32, i64>> {
diff --git a/crates/fluss/src/rpc/message/list_partition_infos.rs 
b/crates/fluss/src/rpc/message/list_partition_infos.rs
index ab693671..cf24f466 100644
--- a/crates/fluss/src/rpc/message/list_partition_infos.rs
+++ b/crates/fluss/src/rpc/message/list_partition_infos.rs
@@ -18,11 +18,10 @@
 use crate::metadata::{PartitionInfo, PartitionSpec, TablePath};
 use crate::proto::ListPartitionInfosResponse;
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::convert::to_table_path;
 use crate::rpc::frame::{ReadError, WriteError};
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
+use crate::{impl_read_type, impl_write_type, proto};
 use bytes::{Buf, BufMut};
 use prost::Message;
 
@@ -46,12 +45,10 @@ impl RequestBody for ListPartitionInfosRequest {
     type ResponseBody = ListPartitionInfosResponse;
 
     const API_KEY: ApiKey = ApiKey::ListPartitionInfos;
-
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(ListPartitionInfosRequest);
-impl_read_version_type!(ListPartitionInfosResponse);
+impl_write_type!(ListPartitionInfosRequest);
+impl_read_type!(ListPartitionInfosResponse);
 
 impl ListPartitionInfosResponse {
     pub fn get_partitions_info(&self) -> Vec<PartitionInfo> {
diff --git a/crates/fluss/src/rpc/message/list_tables.rs 
b/crates/fluss/src/rpc/message/list_tables.rs
index ff2497a0..8ff72141 100644
--- a/crates/fluss/src/rpc/message/list_tables.rs
+++ b/crates/fluss/src/rpc/message/list_tables.rs
@@ -15,15 +15,14 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::{impl_read_type, impl_write_type, proto};
 
 use crate::proto::ListTablesResponse;
 use crate::rpc::frame::ReadError;
 
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
 
 use bytes::{Buf, BufMut};
 use prost::Message;
@@ -47,9 +46,7 @@ impl RequestBody for ListTablesRequest {
     type ResponseBody = ListTablesResponse;
 
     const API_KEY: ApiKey = ApiKey::ListTables;
-
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(ListTablesRequest);
-impl_read_version_type!(ListTablesResponse);
+impl_write_type!(ListTablesRequest);
+impl_read_type!(ListTablesResponse);
diff --git a/crates/fluss/src/rpc/message/lookup.rs 
b/crates/fluss/src/rpc/message/lookup.rs
index 07f8e06c..200d4bc8 100644
--- a/crates/fluss/src/rpc/message/lookup.rs
+++ b/crates/fluss/src/rpc/message/lookup.rs
@@ -19,12 +19,9 @@ use crate::proto::LookupResponse;
 use crate::rpc::frame::ReadError;
 
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
-use crate::{
-    BucketId, PartitionId, TableId, impl_read_version_type, 
impl_write_version_type, proto,
-};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
+use crate::{BucketId, PartitionId, TableId, impl_read_type, impl_write_type, 
proto};
 use bytes::Bytes;
 use prost::Message;
 
@@ -65,9 +62,7 @@ impl RequestBody for LookupRequest {
     type ResponseBody = LookupResponse;
 
     const API_KEY: ApiKey = ApiKey::Lookup;
-
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(LookupRequest);
-impl_read_version_type!(LookupResponse);
+impl_write_type!(LookupRequest);
+impl_read_type!(LookupResponse);
diff --git a/crates/fluss/src/rpc/message/mod.rs 
b/crates/fluss/src/rpc/message/mod.rs
index 456f9cb8..096066ed 100644
--- a/crates/fluss/src/rpc/message/mod.rs
+++ b/crates/fluss/src/rpc/message/mod.rs
@@ -16,10 +16,10 @@
 // under the License.
 
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::{ReadError, WriteError};
 use bytes::{Buf, BufMut};
 
+mod api_versions;
 mod authenticate;
 mod create_database;
 mod create_partition;
@@ -49,6 +49,7 @@ mod table_exists;
 mod update_metadata;
 
 pub use crate::rpc::RpcError;
+pub use api_versions::*;
 pub use authenticate::*;
 pub use create_database::*;
 pub use create_partition::*;
@@ -81,44 +82,36 @@ pub trait RequestBody {
     type ResponseBody;
 
     const API_KEY: ApiKey;
-
-    const REQUEST_VERSION: ApiVersion;
 }
 
 impl<T: RequestBody> RequestBody for &T {
     type ResponseBody = T::ResponseBody;
 
     const API_KEY: ApiKey = T::API_KEY;
-
-    const REQUEST_VERSION: ApiVersion = T::REQUEST_VERSION;
 }
 
-pub trait WriteVersionedType<W>: Sized
+pub trait WriteType<W>: Sized
 where
     W: BufMut,
 {
-    fn write_versioned(&self, writer: &mut W, version: ApiVersion) -> 
Result<(), WriteError>;
+    fn write(&self, writer: &mut W) -> Result<(), WriteError>;
 }
 
-pub trait ReadVersionedType<R>: Sized
+pub trait ReadType<R>: Sized
 where
     R: Buf,
 {
-    fn read_versioned(reader: &mut R, version: ApiVersion) -> Result<Self, 
ReadError>;
+    fn read(reader: &mut R) -> Result<Self, ReadError>;
 }
 
 #[macro_export]
-macro_rules! impl_write_version_type {
+macro_rules! impl_write_type {
     ($type:ty) => {
-        impl<W> WriteVersionedType<W> for $type
+        impl<W> WriteType<W> for $type
         where
             W: BufMut,
         {
-            fn write_versioned(
-                &self,
-                writer: &mut W,
-                _version: ApiVersion,
-            ) -> Result<(), WriteError> {
+            fn write(&self, writer: &mut W) -> Result<(), WriteError> {
                 Ok(self.inner_request.encode(writer).unwrap())
             }
         }
@@ -126,13 +119,13 @@ macro_rules! impl_write_version_type {
 }
 
 #[macro_export]
-macro_rules! impl_read_version_type {
+macro_rules! impl_read_type {
     ($type:ty) => {
-        impl<R> ReadVersionedType<R> for $type
+        impl<R> ReadType<R> for $type
         where
             R: Buf,
         {
-            fn read_versioned(reader: &mut R, _version: ApiVersion) -> 
Result<Self, ReadError> {
+            fn read(reader: &mut R) -> Result<Self, ReadError> {
                 Ok(<$type>::decode(reader).unwrap())
             }
         }
diff --git a/crates/fluss/src/rpc/message/prefix_lookup.rs 
b/crates/fluss/src/rpc/message/prefix_lookup.rs
index 5ee44d25..e71ffe7c 100644
--- a/crates/fluss/src/rpc/message/prefix_lookup.rs
+++ b/crates/fluss/src/rpc/message/prefix_lookup.rs
@@ -19,12 +19,9 @@ use crate::proto::PrefixLookupResponse;
 use crate::rpc::frame::ReadError;
 
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
-use crate::{
-    BucketId, PartitionId, TableId, impl_read_version_type, 
impl_write_version_type, proto,
-};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
+use crate::{BucketId, PartitionId, TableId, impl_read_type, impl_write_type, 
proto};
 use bytes::Bytes;
 use prost::Message;
 
@@ -65,9 +62,7 @@ impl RequestBody for PrefixLookupRequest {
     type ResponseBody = PrefixLookupResponse;
 
     const API_KEY: ApiKey = ApiKey::PrefixLookup;
-
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(PrefixLookupRequest);
-impl_read_version_type!(PrefixLookupResponse);
+impl_write_type!(PrefixLookupRequest);
+impl_read_type!(PrefixLookupResponse);
diff --git a/crates/fluss/src/rpc/message/produce_log.rs 
b/crates/fluss/src/rpc/message/produce_log.rs
index dab7ea9a..8be24638 100644
--- a/crates/fluss/src/rpc/message/produce_log.rs
+++ b/crates/fluss/src/rpc/message/produce_log.rs
@@ -21,10 +21,9 @@ use crate::rpc::frame::ReadError;
 
 use crate::client::ReadyWriteBatch;
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
+use crate::{impl_read_type, impl_write_type, proto};
 use bytes::{Buf, BufMut};
 use prost::Message;
 
@@ -63,9 +62,7 @@ impl RequestBody for ProduceLogRequest {
     type ResponseBody = ProduceLogResponse;
 
     const API_KEY: ApiKey = ApiKey::ProduceLog;
-
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(ProduceLogRequest);
-impl_read_version_type!(ProduceLogResponse);
+impl_write_type!(ProduceLogRequest);
+impl_read_type!(ProduceLogResponse);
diff --git a/crates/fluss/src/rpc/message/put_kv.rs 
b/crates/fluss/src/rpc/message/put_kv.rs
index 983faa66..e76496d1 100644
--- a/crates/fluss/src/rpc/message/put_kv.rs
+++ b/crates/fluss/src/rpc/message/put_kv.rs
@@ -18,11 +18,10 @@
 use crate::client::ReadyWriteBatch;
 use crate::proto::{PbPutKvReqForBucket, PutKvResponse};
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::ReadError;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
+use crate::{impl_read_type, impl_write_type, proto};
 use bytes::{Buf, BufMut};
 use prost::Message;
 
@@ -65,9 +64,7 @@ impl RequestBody for PutKvRequest {
     type ResponseBody = PutKvResponse;
 
     const API_KEY: ApiKey = ApiKey::PutKv;
-
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(PutKvRequest);
-impl_read_version_type!(PutKvResponse);
+impl_write_type!(PutKvRequest);
+impl_read_type!(PutKvResponse);
diff --git a/crates/fluss/src/rpc/message/table_exists.rs 
b/crates/fluss/src/rpc/message/table_exists.rs
index ec982116..5bc848e3 100644
--- a/crates/fluss/src/rpc/message/table_exists.rs
+++ b/crates/fluss/src/rpc/message/table_exists.rs
@@ -16,14 +16,13 @@
 // under the License.
 
 use crate::metadata::TablePath;
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::{impl_read_type, impl_write_type, proto};
 
 use crate::proto::TableExistsResponse;
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::convert::to_table_path;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
 
 use crate::rpc::frame::ReadError;
 
@@ -48,9 +47,7 @@ impl RequestBody for TableExistsRequest {
     type ResponseBody = TableExistsResponse;
 
     const API_KEY: ApiKey = ApiKey::TableExists;
-
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(TableExistsRequest);
-impl_read_version_type!(TableExistsResponse);
+impl_write_type!(TableExistsRequest);
+impl_read_type!(TableExistsResponse);
diff --git a/crates/fluss/src/rpc/message/update_metadata.rs 
b/crates/fluss/src/rpc/message/update_metadata.rs
index 1f0d88c2..fd96ca5e 100644
--- a/crates/fluss/src/rpc/message/update_metadata.rs
+++ b/crates/fluss/src/rpc/message/update_metadata.rs
@@ -18,14 +18,13 @@
 use crate::metadata::{PhysicalTablePath, TablePath};
 use crate::proto::{MetadataResponse, PbPhysicalTablePath, PbTablePath};
 use crate::rpc::api_key::ApiKey;
-use crate::rpc::api_version::ApiVersion;
 use crate::rpc::frame::ReadError;
 use crate::rpc::frame::WriteError;
-use crate::rpc::message::{ReadVersionedType, RequestBody, WriteVersionedType};
+use crate::rpc::message::{ReadType, RequestBody, WriteType};
 use std::collections::HashSet;
 use std::sync::Arc;
 
-use crate::{impl_read_version_type, impl_write_version_type, proto};
+use crate::{impl_read_type, impl_write_type, proto};
 use bytes::{Buf, BufMut};
 use prost::Message;
 
@@ -66,9 +65,7 @@ impl RequestBody for UpdateMetadataRequest {
     type ResponseBody = MetadataResponse;
 
     const API_KEY: ApiKey = ApiKey::MetaData;
-
-    const REQUEST_VERSION: ApiVersion = ApiVersion(0);
 }
 
-impl_write_version_type!(UpdateMetadataRequest);
-impl_read_version_type!(MetadataResponse);
+impl_write_type!(UpdateMetadataRequest);
+impl_read_type!(MetadataResponse);
diff --git a/crates/fluss/src/rpc/server_connection.rs 
b/crates/fluss/src/rpc/server_connection.rs
index 46d99c0d..e1148f9d 100644
--- a/crates/fluss/src/rpc/server_connection.rs
+++ b/crates/fluss/src/rpc/server_connection.rs
@@ -15,20 +15,22 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use crate::cluster::ServerNode;
+use crate::cluster::{ServerNode, ServerType};
 use crate::error::Error;
 use crate::metrics::{
     CLIENT_BYTES_RECEIVED_TOTAL, CLIENT_BYTES_SENT_TOTAL, 
CLIENT_REQUEST_LATENCY_MS,
     CLIENT_REQUESTS_IN_FLIGHT, CLIENT_REQUESTS_TOTAL, CLIENT_RESPONSES_TOTAL, 
LABEL_API_KEY,
     api_key_label,
 };
+use crate::proto::PbApiVersion;
+use crate::rpc::api_key::ApiKey;
 use crate::rpc::api_version::ApiVersion;
 use crate::rpc::error::RpcError;
 use crate::rpc::error::RpcError::ConnectionError;
 use crate::rpc::frame::{AsyncMessageRead, AsyncMessageWrite};
 use crate::rpc::message::{
-    REQUEST_HEADER_LENGTH, ReadVersionedType, RequestBody, RequestHeader, 
ResponseHeader,
-    WriteVersionedType,
+    ApiVersionsRequest, REQUEST_HEADER_LENGTH, ReadType, RequestBody, 
RequestHeader,
+    ResponseHeader, WriteType,
 };
 use crate::rpc::transport::Transport;
 use futures::future::BoxFuture;
@@ -72,6 +74,108 @@ impl fmt::Debug for SaslConfig {
     }
 }
 
+/// Represents the negotiated API versions between the client and a server 
node.
+/// Built from the server's `ApiVersionsResponse` by intersecting each API's
+/// client-supported range with the server-supported range, keeping the highest
+/// usable version.
+#[derive(Clone, Debug)]
+pub struct ServerApiVersions {
+    versions: HashMap<ApiKey, Result<ApiVersion, String>>,
+}
+
+impl ServerApiVersions {
+    /// Build from the server's advertised API version list.
+    pub fn new(server_versions: &[PbApiVersion]) -> Self {
+        let mut versions = HashMap::new();
+        for sv in server_versions {
+            let api_key = ApiKey::from(i16::try_from(sv.api_key).unwrap());
+            // Skip unknown API keys — the client does not support them.
+            let client_range = match api_key.supported_versions() {
+                Some(range) => range,
+                None => continue,
+            };
+            let server_min = i16::try_from(sv.min_version).unwrap();
+            let server_max = i16::try_from(sv.max_version).unwrap();
+            let min_version = client_range.min().0.max(server_min);
+            let max_version = client_range.max().0.min(server_max);
+            if min_version > max_version {
+                versions.insert(
+                    api_key,
+                    Err(format!(
+                        "The server does not support {:?} with version in 
range [{},{}]. \
+                         The supported range is [{},{}].",
+                        api_key,
+                        client_range.min(),
+                        client_range.max(),
+                        server_min,
+                        server_max,
+                    )),
+                );
+            } else {
+                versions.insert(api_key, Ok(ApiVersion(max_version)));
+            }
+        }
+        Self { versions }
+    }
+
+    /// Get the negotiated (highest usable) version for a given API key.
+    pub fn highest_available_version(&self, api_key: ApiKey) -> 
Result<ApiVersion, Error> {
+        match self.versions.get(&api_key) {
+            Some(Ok(version)) => Ok(*version),
+            Some(Err(msg)) => Err(Error::UnsupportedVersion {
+                message: msg.clone(),
+            }),
+            None => Err(Error::UnsupportedVersion {
+                message: format!("The server does not support {:?}", api_key),
+            }),
+        }
+    }
+}
+
+/// Resolve the API version to use for a given API key.
+fn resolve_api_version_for(
+    api_versions: Option<&ServerApiVersions>,
+    api_key: ApiKey,
+) -> Result<ApiVersion, Error> {
+    // version equals highestSupportedVersion might happen when requesting api 
version check
+    // before serverApiVersions is initialized. We always use the highest 
version for api
+    // version checking.
+    let default_version = api_key
+        .supported_versions()
+        .map(|range| range.max())
+        .unwrap();
+    match api_versions {
+        Some(versions) => versions.highest_available_version(api_key),
+        None => Ok(default_version),
+    }
+}
+
+/// Validate that the server's advertised `server_type` matches the type we 
expect
+/// for the target `ServerNode`.
+fn validate_server_type(
+    expected: &ServerType,
+    response_server_type: Option<i32>,
+) -> Result<(), Error> {
+    // For forward-compat with servers that do not populate `server_type`, 
validation is skipped.
+    let Some(type_id) = response_server_type else {
+        return Ok(());
+    };
+    let actual = ServerType::from_type_id(type_id);
+    if actual.as_ref() == Some(expected) {
+        return Ok(());
+    }
+    let actual_desc = actual
+        .map(|t| t.to_string())
+        .unwrap_or_else(|| format!("Unknown(type_id={type_id})"));
+    Err(Error::InvalidServerType {
+        message: format!(
+            "Expected server type {expected} but the server advertised 
{actual_desc}. \
+             The client may be talking to the wrong endpoint \
+             (e.g. coordinator vs tablet server)."
+        ),
+    })
+}
+
 #[derive(Debug, Default)]
 pub struct RpcClient {
     connections: RwLock<HashMap<String, ServerConnection>>,
@@ -142,6 +246,9 @@ impl RpcClient {
         );
         let connection = ServerConnection::new(messenger);
 
+        // Negotiate API versions (must happen before authentication).
+        Self::check_api_versions(&connection, 
server_node.server_type()).await?;
+
         if let Some(ref sasl) = self.sasl_config {
             Self::authenticate(&connection, &sasl.username, 
&sasl.password).await?;
         }
@@ -149,6 +256,20 @@ impl RpcClient {
         Ok(connection)
     }
 
+    /// Send an `ApiVersionsRequest`, validate the advertised `server_type`, 
and
+    /// store the negotiated versions on the connection.
+    async fn check_api_versions(
+        connection: &ServerConnection,
+        expected_server_type: &ServerType,
+    ) -> Result<(), Error> {
+        let request = ApiVersionsRequest::new("fluss-rust", 
env!("CARGO_PKG_VERSION"));
+        let response = connection.request(request).await?;
+        validate_server_type(expected_server_type, response.server_type)?;
+        let api_versions = ServerApiVersions::new(&response.api_versions);
+        *connection.api_versions.lock() = Some(api_versions);
+        Ok(())
+    }
+
     /// Perform SASL/PLAIN authentication handshake.
     ///
     /// Retries on `RetriableAuthenticateException` with exponential backoff
@@ -325,6 +446,10 @@ pub struct ServerConnectionInner<RW> {
 
     state: Arc<Mutex<ConnectionState>>,
 
+    /// Negotiated API versions for this connection.
+    /// `None` until the ApiVersions handshake completes.
+    api_versions: Mutex<Option<ServerApiVersions>>,
+
     join_handle: JoinHandle<()>,
 }
 
@@ -344,16 +469,13 @@ where
                     Ok(msg) => {
                         // message was read, so all subsequent errors should 
not poison the whole stream
                         let mut cursor = Cursor::new(msg);
-                        let header =
-                            match ResponseHeader::read_versioned(&mut cursor, 
ApiVersion(0)) {
-                                Ok(header) => header,
-                                Err(err) => {
-                                    log::warn!(
-                                        "Cannot read message header, ignoring 
message: {err:?}"
-                                    );
-                                    continue;
-                                }
-                            };
+                        let header = match ResponseHeader::read(&mut cursor) {
+                            Ok(header) => header,
+                            Err(err) => {
+                                log::warn!("Cannot read message header, 
ignoring message: {err:?}");
+                                continue;
+                            }
+                        };
 
                         let active_request = match 
state_captured.lock().deref_mut() {
                             ConnectionState::RequestMap(map) => {
@@ -396,10 +518,16 @@ where
             client_id,
             request_id: AtomicI32::new(0),
             state,
+            api_versions: Mutex::new(None),
             join_handle,
         }
     }
 
+    fn resolve_api_version(&self, api_key: ApiKey) -> Result<ApiVersion, 
Error> {
+        let guard = self.api_versions.lock();
+        resolve_api_version_for(guard.as_ref(), api_key)
+    }
+
     fn is_poisoned(&self) -> bool {
         let guard = self.state.lock();
         matches!(*guard, ConnectionState::Poison(_))
@@ -407,29 +535,25 @@ where
 
     pub async fn request<R>(&self, msg: R) -> Result<R::ResponseBody, Error>
     where
-        R: RequestBody + Send + WriteVersionedType<Vec<u8>>,
-        R::ResponseBody: ReadVersionedType<Cursor<Vec<u8>>>,
+        R: RequestBody + Send + WriteType<Vec<u8>>,
+        R::ResponseBody: ReadType<Cursor<Vec<u8>>>,
     {
+        let api_version = self.resolve_api_version(R::API_KEY)?;
         let request_id = self.request_id.fetch_add(1, Ordering::SeqCst) & 
0x7FFFFFFF;
         let header = RequestHeader {
             request_api_key: R::API_KEY,
-            request_api_version: ApiVersion(0),
+            request_api_version: api_version,
             request_id,
             client_id: Some(String::from(self.client_id.as_ref())),
         };
 
-        let header_version = ApiVersion(0);
-
-        let body_api_version = ApiVersion(0);
-
         let mut buf = Vec::new();
         // write header
         header
-            .write_versioned(&mut buf, header_version)
+            .write(&mut buf)
             .map_err(RpcError::WriteMessageError)?;
         // write message body
-        msg.write_versioned(&mut buf, body_api_version)
-            .map_err(RpcError::WriteMessageError)?;
+        msg.write(&mut buf).map_err(RpcError::WriteMessageError)?;
 
         let (tx, rx) = channel();
 
@@ -473,8 +597,7 @@ where
             });
         }
 
-        let body = R::ResponseBody::read_versioned(&mut response.data, 
body_api_version)
-            .map_err(RpcError::ReadMessageError)?;
+        let body = R::ResponseBody::read(&mut 
response.data).map_err(RpcError::ReadMessageError)?;
 
         let read_bytes = response.data.position();
         let message_bytes = response.data.into_inner().len() as u64;
@@ -483,7 +606,7 @@ where
                 message_size: message_bytes,
                 read: read_bytes,
                 api_key: R::API_KEY,
-                api_version: body_api_version,
+                api_version,
             }
             .into());
         }
@@ -643,7 +766,7 @@ mod tests {
     use crate::rpc::ApiKey;
     use crate::rpc::api_version::ApiVersion;
     use crate::rpc::frame::{ReadError, WriteError};
-    use crate::rpc::message::{ReadVersionedType, RequestBody, 
WriteVersionedType};
+    use crate::rpc::message::{ReadType, RequestBody, WriteType};
     use metrics::{SharedString, Unit};
     use metrics_util::CompositeKey;
     use metrics_util::debugging::{DebugValue, DebuggingRecorder};
@@ -659,17 +782,16 @@ mod tests {
     impl RequestBody for TestProduceRequest {
         type ResponseBody = TestProduceResponse;
         const API_KEY: ApiKey = ApiKey::ProduceLog;
-        const REQUEST_VERSION: ApiVersion = ApiVersion(0);
     }
 
-    impl WriteVersionedType<Vec<u8>> for TestProduceRequest {
-        fn write_versioned(&self, _w: &mut Vec<u8>, _v: ApiVersion) -> 
Result<(), WriteError> {
+    impl WriteType<Vec<u8>> for TestProduceRequest {
+        fn write(&self, _w: &mut Vec<u8>) -> Result<(), WriteError> {
             Ok(())
         }
     }
 
-    impl ReadVersionedType<Cursor<Vec<u8>>> for TestProduceResponse {
-        fn read_versioned(_r: &mut Cursor<Vec<u8>>, _v: ApiVersion) -> 
Result<Self, ReadError> {
+    impl ReadType<Cursor<Vec<u8>>> for TestProduceResponse {
+        fn read(_r: &mut Cursor<Vec<u8>>) -> Result<Self, ReadError> {
             Ok(TestProduceResponse)
         }
     }
@@ -680,17 +802,16 @@ mod tests {
     impl RequestBody for TestMetadataRequest {
         type ResponseBody = TestMetadataResponse;
         const API_KEY: ApiKey = ApiKey::MetaData;
-        const REQUEST_VERSION: ApiVersion = ApiVersion(0);
     }
 
-    impl WriteVersionedType<Vec<u8>> for TestMetadataRequest {
-        fn write_versioned(&self, _w: &mut Vec<u8>, _v: ApiVersion) -> 
Result<(), WriteError> {
+    impl WriteType<Vec<u8>> for TestMetadataRequest {
+        fn write(&self, _w: &mut Vec<u8>) -> Result<(), WriteError> {
             Ok(())
         }
     }
 
-    impl ReadVersionedType<Cursor<Vec<u8>>> for TestMetadataResponse {
-        fn read_versioned(_r: &mut Cursor<Vec<u8>>, _v: ApiVersion) -> 
Result<Self, ReadError> {
+    impl ReadType<Cursor<Vec<u8>>> for TestMetadataResponse {
+        fn read(_r: &mut Cursor<Vec<u8>>) -> Result<Self, ReadError> {
             Ok(TestMetadataResponse)
         }
     }
@@ -873,6 +994,11 @@ mod tests {
         tokio::spawn(mock_echo_server(server));
 
         let conn = ServerConnectionInner::new(BufStream::new(client), 
usize::MAX, Arc::from("t"));
+        *conn.api_versions.lock() = Some(ServerApiVersions::new(&[PbApiVersion 
{
+            api_key: 1014,
+            min_version: 0,
+            max_version: 0,
+        }]));
 
         let before: Vec<_> = snapshotter.snapshot().into_vec();
         let request_before = counter_for_label(&before, CLIENT_REQUESTS_TOTAL, 
"produce_log");
@@ -913,6 +1039,11 @@ mod tests {
         tokio::spawn(mock_echo_server(server));
 
         let conn = ServerConnectionInner::new(BufStream::new(client), 
usize::MAX, Arc::from("t"));
+        *conn.api_versions.lock() = Some(ServerApiVersions::new(&[PbApiVersion 
{
+            api_key: 1012,
+            min_version: 0,
+            max_version: 0,
+        }]));
         let before: Vec<_> = snapshotter.snapshot().into_vec();
         let request_sum_before = counter_sum(&before, CLIENT_REQUESTS_TOTAL);
         let response_sum_before = counter_sum(&before, CLIENT_RESPONSES_TOTAL);
@@ -949,6 +1080,11 @@ mod tests {
         let (client, server) = tokio::io::duplex(64);
         drop(server); // force write failure on request path
         let conn = ServerConnectionInner::new(BufStream::new(client), 
usize::MAX, Arc::from("t"));
+        *conn.api_versions.lock() = Some(ServerApiVersions::new(&[PbApiVersion 
{
+            api_key: 1014,
+            min_version: 0,
+            max_version: 0,
+        }]));
 
         let before: Vec<_> = snapshotter.snapshot().into_vec();
         let request_before = counter_for_label(&before, CLIENT_REQUESTS_TOTAL, 
"produce_log");
@@ -997,6 +1133,11 @@ mod tests {
         tokio::spawn(mock_error_server(server));
 
         let conn = ServerConnectionInner::new(BufStream::new(client), 
usize::MAX, Arc::from("t"));
+        *conn.api_versions.lock() = Some(ServerApiVersions::new(&[PbApiVersion 
{
+            api_key: 1014,
+            min_version: 0,
+            max_version: 0,
+        }]));
 
         let before: Vec<_> = snapshotter.snapshot().into_vec();
         let response_before = counter_for_label(&before, 
CLIENT_RESPONSES_TOTAL, "produce_log");
@@ -1030,4 +1171,130 @@ mod tests {
             "in-flight gauge must return to zero after API error"
         );
     }
+
+    #[tokio::test]
+    async fn server_api_versions_negotiation() {
+        assert_eq!(
+            resolve_api_version_for(None, ApiKey::ApiVersion).unwrap(),
+            ApiVersion(0)
+        );
+
+        assert_eq!(
+            resolve_api_version_for(None, ApiKey::PutKv).unwrap(),
+            ApiVersion(0)
+        );
+
+        let server_versions = vec![
+            // PutKv: server v0..v3, client v0 only (v1 key encoding not yet 
implemented) → negotiated v0
+            PbApiVersion {
+                api_key: 1016,
+                min_version: 0,
+                max_version: 3,
+            },
+            // ProduceLog: server v0..v2, client v0 only → negotiated v0
+            PbApiVersion {
+                api_key: 1014,
+                min_version: 0,
+                max_version: 2,
+            },
+            // Disjoint: server v5..v7, client v0 only → error
+            PbApiVersion {
+                api_key: 1015,
+                min_version: 5,
+                max_version: 7,
+            },
+            // Unknown key (9999) → skipped
+            PbApiVersion {
+                api_key: 9999,
+                min_version: 0,
+                max_version: 5,
+            },
+        ];
+        let negotiated = ServerApiVersions::new(&server_versions);
+
+        // Successful negotiation cases
+        assert_eq!(
+            negotiated.highest_available_version(ApiKey::PutKv).unwrap(),
+            ApiVersion(0)
+        );
+        assert_eq!(
+            negotiated
+                .highest_available_version(ApiKey::ProduceLog)
+                .unwrap(),
+            ApiVersion(0)
+        );
+
+        // Disjoint range → error
+        assert!(
+            negotiated
+                .highest_available_version(ApiKey::FetchLog)
+                .unwrap_err()
+                .to_string()
+                .contains(&format!(
+                    "The server does not support {:?}",
+                    ApiKey::FetchLog
+                ))
+        );
+
+        // Unknown key is skipped → not in map → error
+        assert!(
+            negotiated
+                .highest_available_version(ApiKey::Unknown(9999))
+                .is_err()
+        );
+
+        // Key not advertised by server → error
+        assert!(
+            ServerApiVersions::new(&[])
+                .highest_available_version(ApiKey::FetchLog)
+                .is_err()
+        );
+    }
+
+    #[test]
+    fn server_type_validation() {
+        // Happy path: server advertises the expected type.
+        assert!(
+            validate_server_type(
+                &ServerType::CoordinatorServer,
+                Some(ServerType::CoordinatorServer.to_type_id()),
+            )
+            .is_ok()
+        );
+        assert!(
+            validate_server_type(
+                &ServerType::TabletServer,
+                Some(ServerType::TabletServer.to_type_id()),
+            )
+            .is_ok()
+        );
+
+        // Mismatch: connected to a coordinator while expecting a tablet server
+        // (and vice versa).
+        let err = validate_server_type(
+            &ServerType::TabletServer,
+            Some(ServerType::CoordinatorServer.to_type_id()),
+        )
+        .unwrap_err();
+        assert!(
+            matches!(err, Error::InvalidServerType { .. }),
+            "expected InvalidServerType, got: {err:?}"
+        );
+
+        assert!(matches!(
+            validate_server_type(
+                &ServerType::CoordinatorServer,
+                Some(ServerType::TabletServer.to_type_id()),
+            ),
+            Err(Error::InvalidServerType { .. })
+        ));
+
+        validate_server_type(&ServerType::TabletServer, None).ok();
+        // Unknown / unmapped type id still fails, with the raw id surfaced so
+        // operators can diagnose protocol drift.
+        assert!(matches!(
+            validate_server_type(&ServerType::CoordinatorServer, Some(99),),
+            Err(Error::InvalidServerType { .. })
+        ));
+    }
 }
diff --git a/crates/fluss/tests/integration/kv_table.rs 
b/crates/fluss/tests/integration/kv_table.rs
index 62e206b6..1979b973 100644
--- a/crates/fluss/tests/integration/kv_table.rs
+++ b/crates/fluss/tests/integration/kv_table.rs
@@ -1404,4 +1404,75 @@ mod kv_table_test {
             .await
             .expect("Failed to drop table");
     }
+
+    /// Test that KV format v2 tables with non-default bucket key reject v0 
clients.
+    /// The Rust client currently only supports API version 0 for 
PutKv/Lookup/PrefixLookup.
+    /// When the server creates a table with kv_format_version=2 and a 
non-default bucket key,
+    /// it rejects v0 clients because CompactedKeyEncoder (v1) is required.
+    // TODO(key-encoding-v1): Once v1 key encoding is implemented and the 
client advertises
+    //  PutKv/Lookup/PrefixLookup v1, this test should be updated to verify 
that v1 clients
+    //  can successfully write to and read from kv_format_v2 tables with 
non-default bucket keys.
+    #[tokio::test]
+    async fn kv_format_v2_table_rejects_v0_client() {
+        let cluster = get_shared_cluster();
+        let connection = cluster.get_fluss_connection().await;
+
+        let admin = connection.get_admin().unwrap();
+
+        let table_path = TablePath::new("fluss", 
"test_kv_format_v2_reject_v0");
+
+        // Create a KV table with:
+        // 1. kv_format_version = 2
+        // 2. non-default bucket key ("a" is a subset of pk ("a", "b"))
+        // 3. datalake format is exist.
+        let table_descriptor = TableDescriptor::builder()
+            .schema(
+                Schema::builder()
+                    .column("a", DataTypes::int())
+                    .column("b", DataTypes::string())
+                    .column("c", DataTypes::string())
+                    .primary_key(vec!["a", "b"])
+                    .build()
+                    .expect("Failed to build schema"),
+            )
+            .distributed_by(Some(2), vec!["a".to_string()])
+            .property("table.kv.format-version", "2")
+            .property("table.datalake.format", "lance")
+            .build()
+            .expect("Failed to build table");
+
+        create_table(&admin, &table_path, &table_descriptor).await;
+
+        let table = connection.get_table(&table_path).await.unwrap();
+
+        // Test PutKv with v0 client - should fail with UNSUPPORTED_VERSION
+        let table_upsert = table.new_upsert().expect("Failed to create 
upsert");
+        let upsert_writer = table_upsert
+            .create_writer()
+            .expect("Failed to create writer");
+
+        let mut row = GenericRow::new(3);
+        row.set_field(0, 1);
+        row.set_field(1, "a");
+        row.set_field(2, "value1");
+        let upsert_result = upsert_writer
+            .upsert(&row)
+            .expect("Failed to upsert row")
+            .await;
+        assert!(
+            upsert_result.is_err(),
+            "PutKv with v0 client should be rejected for kv_format_v2 table 
with non-default bucket key"
+        );
+        let err_msg = upsert_result.unwrap_err().to_string();
+        assert!(
+            err_msg.contains("Client API version 0 is not supported"),
+            "Expected 'Client API version 0 is not supported' error, got: {}",
+            err_msg
+        );
+
+        admin
+            .drop_table(&table_path, false)
+            .await
+            .expect("Failed to drop table");
+    }
 }

Reply via email to