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 0dae3160 [rust] Add Fluss 1.x protocol support to the admin client
(#631)
0dae3160 is described below
commit 0dae3160fb6a8729cf2541f393e1c08dcac80f02
Author: Pengcheng Huang <[email protected]>
AuthorDate: Fri Jun 26 19:10:33 2026 +0800
[rust] Add Fluss 1.x protocol support to the admin client (#631)
* [rust] Add Fluss 1.x protocol support to the admin client
Add 27 new admin methods to FlussAdmin:
- Database/table extensions: list_database_summaries, alter_database,
alter_table, get_table_stats
- KV snapshot operations: get_latest_kv_snapshots,
get_kv_snapshot_metadata, create_kv_snapshot_lease, get_lake_snapshot
- ACL management: create_acls, list_acls, drop_acls
- Cluster configuration: describe_cluster_configs, alter_cluster_configs
- Server management: add_server_tag, remove_server_tag, rebalance,
list_rebalance_progress, cancel_rebalance
- Producer offsets: register_producer_offsets, get_producer_offsets,
delete_producer_offsets
- Monitoring: get_cluster_health, list_remote_log_manifests
- KV snapshots: list_kv_snapshots, release_kv_snapshot_lease,
drop_kv_snapshot_lease
* Address reviewer feedback: add domain enums, type aliases, expose readable
- Add ClusterHealthStatus enum (Green/Yellow/Red/Unknown) to
cluster_health.rs
- Add RebalanceStatus enum (NotStarted..Timeout) to rebalance.rs
- Replace raw i32/i64 with BucketId/TableId/PartitionId aliases in
kv_snapshot.rs, lake_snapshot.rs, rebalance.rs, producer_offsets.rs
- Expose readable parameter in admin.get_lake_snapshot() instead of
hardcoding None
Addresses reviewer feedback on PR #631.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Apply type aliases to remaining metadata types (table_stats,
kv_snapshot_lease)
Extends the BucketId/TableId/PartitionId alias consistency fix to
table_stats.rs (BucketStatsRequest, BucketStats) and
kv_snapshot_lease.rs (KvSnapshotLeaseForBucket, KvSnapshotLeaseForTable).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Update admin.rs to use GoalType/ServerTag enums after rebase
Now that pr/3 provides GoalType and ServerTag enums in the RPC
wrappers, update the admin client methods to use them in their
public signatures too.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Apply TableId/PartitionId/BucketId type aliases to admin methods
Update the new admin method signatures introduced by this PR to use
the i64/i32 type aliases from lib.rs instead of raw primitives, matching
the underlying RPC message wrappers.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
* Address reviewer feedback round 2
- Add `RegisterProducerResult` enum mirroring Java's `RegisterResult`
(Created=0, AlreadyExists=1); `register_producer_offsets` now returns
`Option<RegisterProducerResult>` instead of raw `Option<i32>`.
- Expose `comment: Option<&str>` on `alter_database` and the
`AlterDatabaseRequest` wrapper (was hardcoded to `None`).
- Replace fully-qualified paths with imports: `BucketStatsRequest` in
admin.rs, `PbDatabaseSummary` in metadata/database.rs, and
`AlterConfig` self-reference in metadata/table_change.rs.
- Rename `create_acls(acl)` to `create_acls(acls)` and
`drop_acls(acl_filter)` to `drop_acls(acl_filters)` for plural
consistency with the `Vec` argument type.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
---------
Co-authored-by: warmbupt <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
crates/fluss/src/client/admin.rs | 407 ++++++++++++++++++++-
crates/fluss/src/metadata/acl.rs | 74 +++-
crates/fluss/src/metadata/cluster_health.rs | 109 ++++++
crates/fluss/src/metadata/config.rs | 29 +-
crates/fluss/src/metadata/database.rs | 19 +
crates/fluss/src/metadata/kv_snapshot.rs | 171 +++++++++
crates/fluss/src/metadata/kv_snapshot_lease.rs | 7 +-
crates/fluss/src/metadata/lake_snapshot.rs | 83 +++++
crates/fluss/src/metadata/mod.rs | 12 +
crates/fluss/src/metadata/producer_offsets.rs | 31 +-
crates/fluss/src/metadata/rebalance.rs | 187 ++++++++++
.../fluss/src/metadata/register_producer_result.rs | 66 ++++
crates/fluss/src/metadata/remote_log.rs | 59 +++
crates/fluss/src/metadata/table_change.rs | 13 +
crates/fluss/src/metadata/table_stats.rs | 83 ++++-
crates/fluss/src/rpc/message/alter_database.rs | 3 +-
16 files changed, 1334 insertions(+), 19 deletions(-)
diff --git a/crates/fluss/src/client/admin.rs b/crates/fluss/src/client/admin.rs
index d3d5a5e3..7015e573 100644
--- a/crates/fluss/src/client/admin.rs
+++ b/crates/fluss/src/client/admin.rs
@@ -18,14 +18,27 @@
use crate::client::metadata::Metadata;
use crate::cluster::ServerNode;
use crate::metadata::{
- DatabaseDescriptor, DatabaseInfo, JsonSerde, LakeSnapshot, PartitionInfo,
PartitionSpec,
- PhysicalTablePath, Schema, SchemaInfo, TableBucket, TableDescriptor,
TableInfo, TablePath,
+ AclFilter, AclInfo, AcquireKvSnapshotLeaseResult, ActiveKvSnapshots,
AlterConfig,
+ AlterTableChanges, BucketStatsRequest, ClusterHealth, CreateAclResult,
DatabaseDescriptor,
+ DatabaseInfo, DatabaseSummary, DescribeConfig, DropAclsFilterResult,
GoalType, JsonSerde,
+ KvSnapshotLeaseForTable, KvSnapshotMetadata, LakeSnapshot,
LakeSnapshotInfo, LatestKvSnapshots,
+ PartitionInfo, PartitionSpec, PhysicalTablePath, ProducerOffsets,
ProducerTableOffsets,
+ RebalanceProgress, RegisterProducerResult, RemoteLogManifestEntry, Schema,
SchemaInfo,
+ ServerTag, TableBucket, TableDescriptor, TableInfo, TablePath, TableStats,
};
use crate::rpc::message::{
+ AcquireKvSnapshotLeaseRequest, AddServerTagRequest,
AlterClusterConfigsRequest,
+ AlterDatabaseRequest, AlterTableRequest, CancelRebalanceRequest,
CreateAclsRequest,
CreateDatabaseRequest, CreatePartitionRequest, CreateTableRequest,
DatabaseExistsRequest,
- DropDatabaseRequest, DropPartitionRequest, DropTableRequest,
GetDatabaseInfoRequest,
- GetLatestLakeSnapshotRequest, GetTableRequest, GetTableSchemaRequestMsg,
ListDatabasesRequest,
- ListPartitionInfosRequest, ListTablesRequest, TableExistsRequest,
+ DeleteProducerOffsetsRequest, DescribeClusterConfigsRequest,
DropAclsRequest,
+ DropDatabaseRequest, DropKvSnapshotLeaseRequest, DropPartitionRequest,
DropTableRequest,
+ GetClusterHealthRequest, GetDatabaseInfoRequest,
GetKvSnapshotMetadataRequest,
+ GetLakeSnapshotRequest, GetLatestKvSnapshotsRequest,
GetLatestLakeSnapshotRequest,
+ GetProducerOffsetsRequest, GetTableRequest, GetTableSchemaRequestMsg,
GetTableStatsRequest,
+ ListAclsRequest, ListDatabaseSummariesRequest, ListDatabasesRequest,
ListKvSnapshotsRequest,
+ ListPartitionInfosRequest, ListRebalanceProgressRequest,
ListRemoteLogManifestsRequest,
+ ListTablesRequest, RebalanceRequest, RegisterProducerOffsetsRequest,
+ ReleaseKvSnapshotLeaseRequest, RemoveServerTagRequest, TableExistsRequest,
};
use crate::rpc::message::{ListOffsetsRequest, OffsetSpec};
use crate::rpc::{RpcClient, ServerConnection};
@@ -483,4 +496,388 @@ impl FlussAdmin {
}
Ok(tasks)
}
+
+ /// List database summaries (name, created_time, table_count).
+ pub async fn list_database_summaries(&self) ->
Result<Vec<DatabaseSummary>> {
+ let response = self
+ .admin_gateway()
+ .await?
+ .request(ListDatabaseSummariesRequest::new())
+ .await?;
+ Ok(response
+ .database_summary
+ .iter()
+ .map(DatabaseSummary::from_pb)
+ .collect())
+ }
+
+ /// Alter a database: config changes and/or an updated comment.
+ pub async fn alter_database(
+ &self,
+ name: &str,
+ config_changes: Vec<AlterConfig>,
+ comment: Option<&str>,
+ ignore_if_not_exists: bool,
+ ) -> Result<()> {
+ let _response = self
+ .admin_gateway()
+ .await?
+ .request(AlterDatabaseRequest::new(
+ name,
+ ignore_if_not_exists,
+ config_changes,
+ comment,
+ ))
+ .await?;
+ Ok(())
+ }
+
+ /// Alter a table: config changes plus any combination of
add/drop/rename/modify columns.
+ /// Bundle the column-level edits in [`AlterTableChanges`].
+ pub async fn alter_table(
+ &self,
+ table_path: &TablePath,
+ ignore_if_not_exists: bool,
+ changes: AlterTableChanges,
+ ) -> Result<()> {
+ let _response = self
+ .admin_gateway()
+ .await?
+ .request(AlterTableRequest::new(
+ table_path,
+ ignore_if_not_exists,
+ changes.config_changes,
+ changes.add_columns,
+ changes.drop_columns,
+ changes.rename_columns,
+ changes.modify_columns,
+ ))
+ .await?;
+ Ok(())
+ }
+
+ /// Get table statistics for buckets. Pass empty `target_columns` to
request stats for all columns.
+ pub async fn get_table_stats(
+ &self,
+ table_id: TableId,
+ buckets_req: Vec<BucketStatsRequest>,
+ target_columns: Vec<i32>,
+ ) -> Result<TableStats> {
+ let response = self
+ .admin_gateway()
+ .await?
+ .request(GetTableStatsRequest::new(
+ table_id,
+ buckets_req,
+ target_columns,
+ ))
+ .await?;
+ Ok(TableStats::from_pb(&response))
+ }
+
+ /// Get the latest KV snapshots for a table (optionally scoped to one
partition).
+ pub async fn get_latest_kv_snapshots(
+ &self,
+ table_path: &TablePath,
+ partition_name: Option<&str>,
+ ) -> Result<LatestKvSnapshots> {
+ let response = self
+ .admin_gateway()
+ .await?
+ .request(GetLatestKvSnapshotsRequest::new(table_path,
partition_name))
+ .await?;
+ Ok(LatestKvSnapshots::from_pb(&response))
+ }
+
+ /// Get KV snapshot metadata (manifest file list).
+ pub async fn get_kv_snapshot_metadata(
+ &self,
+ table_id: TableId,
+ partition_id: Option<PartitionId>,
+ bucket_id: BucketId,
+ snapshot_id: i64,
+ ) -> Result<KvSnapshotMetadata> {
+ let response = self
+ .admin_gateway()
+ .await?
+ .request(GetKvSnapshotMetadataRequest::new(
+ table_id,
+ partition_id,
+ bucket_id,
+ snapshot_id,
+ ))
+ .await?;
+ Ok(KvSnapshotMetadata::from_pb(&response))
+ }
+
+ /// Acquire a KV snapshot lease. Returns the snapshots the server could
not lease.
+ pub async fn create_kv_snapshot_lease(
+ &self,
+ lease_id: &str,
+ lease_duration_ms: i64,
+ snapshots_to_lease: Vec<KvSnapshotLeaseForTable>,
+ ) -> Result<AcquireKvSnapshotLeaseResult> {
+ let response = self
+ .admin_gateway()
+ .await?
+ .request(AcquireKvSnapshotLeaseRequest::new(
+ lease_id,
+ lease_duration_ms,
+ snapshots_to_lease,
+ ))
+ .await?;
+ Ok(AcquireKvSnapshotLeaseResult::from_pb(&response))
+ }
+
+ /// Get a specific lake snapshot for a table.
+ pub async fn get_lake_snapshot(
+ &self,
+ table_path: &TablePath,
+ snapshot_id: Option<i64>,
+ readable: Option<bool>,
+ ) -> Result<LakeSnapshotInfo> {
+ let response = self
+ .admin_gateway()
+ .await?
+ .request(GetLakeSnapshotRequest::new(
+ table_path,
+ snapshot_id,
+ readable,
+ ))
+ .await?;
+ Ok(LakeSnapshotInfo::from_pb(&response))
+ }
+
+ /// Create ACLs. Returns one result per submitted ACL (success or per-ACL
error).
+ pub async fn create_acls(&self, acls: Vec<AclInfo>) ->
Result<Vec<CreateAclResult>> {
+ let response = self
+ .admin_gateway()
+ .await?
+ .request(CreateAclsRequest::new(acls))
+ .await?;
+ response
+ .acl_res
+ .iter()
+ .map(CreateAclResult::from_pb)
+ .collect()
+ }
+
+ /// List ACLs matching a filter.
+ pub async fn list_acls(&self, acl_filter: AclFilter) ->
Result<Vec<AclInfo>> {
+ let response = self
+ .admin_gateway()
+ .await?
+ .request(ListAclsRequest::new(acl_filter))
+ .await?;
+ response.acl.iter().map(AclInfo::from_pb).collect()
+ }
+
+ /// Drop ACLs matching filters. Returns one result per submitted filter.
+ pub async fn drop_acls(
+ &self,
+ acl_filters: Vec<AclFilter>,
+ ) -> Result<Vec<DropAclsFilterResult>> {
+ let response = self
+ .admin_gateway()
+ .await?
+ .request(DropAclsRequest::new(acl_filters))
+ .await?;
+ response
+ .filter_results
+ .iter()
+ .map(DropAclsFilterResult::from_pb)
+ .collect()
+ }
+
+ /// Describe cluster configuration.
+ pub async fn describe_cluster_configs(&self) ->
Result<Vec<DescribeConfig>> {
+ let response = self
+ .admin_gateway()
+ .await?
+ .request(DescribeClusterConfigsRequest::new())
+ .await?;
+ Ok(response
+ .configs
+ .iter()
+ .map(DescribeConfig::from_pb)
+ .collect())
+ }
+
+ /// Alter cluster configuration.
+ pub async fn alter_cluster_configs(&self, alter_configs: Vec<AlterConfig>)
-> Result<()> {
+ let _response = self
+ .admin_gateway()
+ .await?
+ .request(AlterClusterConfigsRequest::new(alter_configs))
+ .await?;
+ Ok(())
+ }
+
+ /// Add a tag to servers.
+ pub async fn add_server_tag(&self, server_ids: Vec<i32>, server_tag:
ServerTag) -> Result<()> {
+ let _response = self
+ .admin_gateway()
+ .await?
+ .request(AddServerTagRequest::new(server_ids, server_tag))
+ .await?;
+ Ok(())
+ }
+
+ /// Remove a tag from servers.
+ pub async fn remove_server_tag(
+ &self,
+ server_ids: Vec<i32>,
+ server_tag: ServerTag,
+ ) -> Result<()> {
+ let _response = self
+ .admin_gateway()
+ .await?
+ .request(RemoveServerTagRequest::new(server_ids, server_tag))
+ .await?;
+ Ok(())
+ }
+
+ /// Trigger a rebalance. Returns the rebalance id assigned by the server.
+ pub async fn rebalance(&self, goals: Vec<GoalType>) -> Result<String> {
+ let response = self
+ .admin_gateway()
+ .await?
+ .request(RebalanceRequest::new(goals))
+ .await?;
+ Ok(response.rebalance_id)
+ }
+
+ /// List rebalance progress (for a specific rebalance id, or all in-flight
ones if `None`).
+ pub async fn list_rebalance_progress(
+ &self,
+ rebalance_id: Option<&str>,
+ ) -> Result<RebalanceProgress> {
+ let response = self
+ .admin_gateway()
+ .await?
+ .request(ListRebalanceProgressRequest::new(rebalance_id))
+ .await?;
+ RebalanceProgress::from_pb(&response)
+ }
+
+ /// Cancel a rebalance.
+ pub async fn cancel_rebalance(&self, rebalance_id: Option<&str>) ->
Result<()> {
+ let _response = self
+ .admin_gateway()
+ .await?
+ .request(CancelRebalanceRequest::new(rebalance_id))
+ .await?;
+ Ok(())
+ }
+
+ /// Register producer offsets. Returns the server-side registration
outcome (if any).
+ pub async fn register_producer_offsets(
+ &self,
+ producer_id: &str,
+ table_offsets: Vec<ProducerTableOffsets>,
+ ttl_ms: Option<i64>,
+ ) -> Result<Option<RegisterProducerResult>> {
+ let response = self
+ .admin_gateway()
+ .await?
+ .request(RegisterProducerOffsetsRequest::new(
+ producer_id,
+ table_offsets,
+ ttl_ms,
+ ))
+ .await?;
+ response
+ .result
+ .map(RegisterProducerResult::try_from_i32)
+ .transpose()
+ }
+
+ /// Get producer offsets.
+ pub async fn get_producer_offsets(&self, producer_id: &str) ->
Result<ProducerOffsets> {
+ let response = self
+ .admin_gateway()
+ .await?
+ .request(GetProducerOffsetsRequest::new(producer_id))
+ .await?;
+ Ok(ProducerOffsets::from_pb(&response))
+ }
+
+ /// Delete producer offsets.
+ pub async fn delete_producer_offsets(&self, producer_id: &str) ->
Result<()> {
+ let _response = self
+ .admin_gateway()
+ .await?
+ .request(DeleteProducerOffsetsRequest::new(producer_id))
+ .await?;
+ Ok(())
+ }
+
+ /// Get cluster health status.
+ pub async fn get_cluster_health(&self) -> Result<ClusterHealth> {
+ let response = self
+ .admin_gateway()
+ .await?
+ .request(GetClusterHealthRequest::new())
+ .await?;
+ ClusterHealth::from_pb(&response)
+ }
+
+ /// List remote log manifests for a table (optionally scoped to one
partition).
+ pub async fn list_remote_log_manifests(
+ &self,
+ table_id: TableId,
+ partition_id: Option<PartitionId>,
+ ) -> Result<Vec<RemoteLogManifestEntry>> {
+ let response = self
+ .admin_gateway()
+ .await?
+ .request(ListRemoteLogManifestsRequest::new(table_id,
partition_id))
+ .await?;
+ Ok(response
+ .manifests
+ .iter()
+ .map(RemoteLogManifestEntry::from_pb)
+ .collect())
+ }
+
+ /// List active KV snapshots for a table (optionally scoped to one
partition).
+ pub async fn list_kv_snapshots(
+ &self,
+ table_id: TableId,
+ partition_id: Option<PartitionId>,
+ ) -> Result<ActiveKvSnapshots> {
+ let response = self
+ .admin_gateway()
+ .await?
+ .request(ListKvSnapshotsRequest::new(table_id, partition_id))
+ .await?;
+ Ok(ActiveKvSnapshots::from_pb(&response))
+ }
+
+ /// Release specific bucket snapshots from a KV snapshot lease.
+ pub async fn release_kv_snapshot_lease(
+ &self,
+ lease_id: &str,
+ buckets_to_release: Vec<TableBucket>,
+ ) -> Result<()> {
+ let _response = self
+ .admin_gateway()
+ .await?
+ .request(ReleaseKvSnapshotLeaseRequest::new(
+ lease_id,
+ buckets_to_release,
+ ))
+ .await?;
+ Ok(())
+ }
+
+ /// Drop an entire KV snapshot lease.
+ pub async fn drop_kv_snapshot_lease(&self, lease_id: &str) -> Result<()> {
+ let _response = self
+ .admin_gateway()
+ .await?
+ .request(DropKvSnapshotLeaseRequest::new(lease_id))
+ .await?;
+ Ok(())
+ }
}
diff --git a/crates/fluss/src/metadata/acl.rs b/crates/fluss/src/metadata/acl.rs
index 84ec97af..f69ec952 100644
--- a/crates/fluss/src/metadata/acl.rs
+++ b/crates/fluss/src/metadata/acl.rs
@@ -16,7 +16,9 @@
// under the License.
use crate::error::{Error, Result};
-use crate::proto::{PbAclFilter, PbAclInfo};
+use crate::proto::{
+ PbAclFilter, PbAclInfo, PbCreateAclRespInfo, PbDropAclsFilterResult,
PbDropAclsMatchingAcl,
+};
/// Mirrors Java `org.apache.fluss.security.acl.ResourceType`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -197,6 +199,76 @@ impl AclFilter {
}
}
+/// One per ACL submitted to `create_acls`: success or a server-side error.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct CreateAclResult {
+ pub acl: AclInfo,
+ pub error: Option<AclError>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct AclError {
+ pub code: i32,
+ pub message: Option<String>,
+}
+
+impl CreateAclResult {
+ pub fn from_pb(pb: &PbCreateAclRespInfo) -> Result<Self> {
+ Ok(Self {
+ acl: AclInfo::from_pb(&pb.acl)?,
+ error: pb.error_code.map(|code| AclError {
+ code,
+ message: pb.error_message.clone(),
+ }),
+ })
+ }
+}
+
+/// One ACL matched by a filter in `drop_acls`. Reports the bound ACL and any
+/// server-side error encountered while dropping it.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct DropAclMatchingAcl {
+ pub acl: AclInfo,
+ pub error: Option<AclError>,
+}
+
+impl DropAclMatchingAcl {
+ pub fn from_pb(pb: &PbDropAclsMatchingAcl) -> Result<Self> {
+ Ok(Self {
+ acl: AclInfo::from_pb(&pb.acl)?,
+ error: pb.error_code.map(|code| AclError {
+ code,
+ message: pb.error_message.clone(),
+ }),
+ })
+ }
+}
+
+/// One per filter submitted to `drop_acls`: the matching ACLs that were
+/// targeted plus any filter-level error.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct DropAclsFilterResult {
+ pub matching_acls: Vec<DropAclMatchingAcl>,
+ pub error: Option<AclError>,
+}
+
+impl DropAclsFilterResult {
+ pub fn from_pb(pb: &PbDropAclsFilterResult) -> Result<Self> {
+ let matching_acls = pb
+ .matching_acls
+ .iter()
+ .map(DropAclMatchingAcl::from_pb)
+ .collect::<Result<Vec<_>>>()?;
+ Ok(Self {
+ matching_acls,
+ error: pb.error_code.map(|code| AclError {
+ code,
+ message: pb.error_message.clone(),
+ }),
+ })
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/crates/fluss/src/metadata/cluster_health.rs
b/crates/fluss/src/metadata/cluster_health.rs
new file mode 100644
index 00000000..938d1f5f
--- /dev/null
+++ b/crates/fluss/src/metadata/cluster_health.rs
@@ -0,0 +1,109 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::error::{Error, Result};
+use crate::proto::GetClusterHealthResponse;
+
+/// Mirrors Java `org.apache.fluss.client.admin.ClusterHealthStatus`.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ClusterHealthStatus {
+ Green,
+ Yellow,
+ Red,
+ Unknown,
+}
+
+impl ClusterHealthStatus {
+ pub fn to_i32(self) -> i32 {
+ match self {
+ Self::Green => 0,
+ Self::Yellow => 1,
+ Self::Red => 2,
+ Self::Unknown => 3,
+ }
+ }
+
+ pub fn try_from_i32(value: i32) -> Result<Self> {
+ match value {
+ 0 => Ok(Self::Green),
+ 1 => Ok(Self::Yellow),
+ 2 => Ok(Self::Red),
+ 3 => Ok(Self::Unknown),
+ _ => Err(Error::IllegalArgument {
+ message: format!("Unsupported ClusterHealthStatus: {value}"),
+ }),
+ }
+ }
+}
+
+/// Result of `get_cluster_health`.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ClusterHealth {
+ pub num_replicas: i32,
+ pub in_sync_replicas: i32,
+ pub num_leader_replicas: i32,
+ pub active_leader_replicas: i32,
+ pub status: ClusterHealthStatus,
+}
+
+impl ClusterHealth {
+ pub fn from_pb(pb: &GetClusterHealthResponse) -> Result<Self> {
+ Ok(Self {
+ num_replicas: pb.num_replicas,
+ in_sync_replicas: pb.in_sync_replicas,
+ num_leader_replicas: pb.num_leader_replicas,
+ active_leader_replicas: pb.active_leader_replicas,
+ status: ClusterHealthStatus::try_from_i32(pb.status)?,
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_cluster_health_status_roundtrip() {
+ for s in [
+ ClusterHealthStatus::Green,
+ ClusterHealthStatus::Yellow,
+ ClusterHealthStatus::Red,
+ ClusterHealthStatus::Unknown,
+ ] {
+ assert_eq!(ClusterHealthStatus::try_from_i32(s.to_i32()).unwrap(),
s);
+ }
+ }
+
+ #[test]
+ fn test_cluster_health_status_unknown_value() {
+ assert!(ClusterHealthStatus::try_from_i32(99).is_err());
+ }
+
+ #[test]
+ fn test_cluster_health_from_pb() {
+ let pb = GetClusterHealthResponse {
+ num_replicas: 5,
+ in_sync_replicas: 4,
+ num_leader_replicas: 3,
+ active_leader_replicas: 3,
+ status: 1,
+ };
+ let h = ClusterHealth::from_pb(&pb).unwrap();
+ assert_eq!(h.num_replicas, 5);
+ assert_eq!(h.status, ClusterHealthStatus::Yellow);
+ }
+}
diff --git a/crates/fluss/src/metadata/config.rs
b/crates/fluss/src/metadata/config.rs
index 4e045c45..4dd7d8bc 100644
--- a/crates/fluss/src/metadata/config.rs
+++ b/crates/fluss/src/metadata/config.rs
@@ -16,7 +16,7 @@
// under the License.
use crate::error::{Error, Result};
-use crate::proto::PbAlterConfig;
+use crate::proto::{PbAlterConfig, PbDescribeConfig};
/// Mirrors Java `org.apache.fluss.config.cluster.AlterConfigOpType`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -88,6 +88,33 @@ impl AlterConfig {
}
}
+/// One entry in the response of `describe_cluster_configs`. Mirrors Java's
+/// `org.apache.fluss.config.cluster.DescribeConfig`.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct DescribeConfig {
+ pub config_key: String,
+ pub config_value: Option<String>,
+ pub config_source: String,
+}
+
+impl DescribeConfig {
+ pub fn from_pb(pb: &PbDescribeConfig) -> Self {
+ Self {
+ config_key: pb.config_key.clone(),
+ config_value: pb.config_value.clone(),
+ config_source: pb.config_source.clone(),
+ }
+ }
+
+ pub fn to_pb(&self) -> PbDescribeConfig {
+ PbDescribeConfig {
+ config_key: self.config_key.clone(),
+ config_value: self.config_value.clone(),
+ config_source: self.config_source.clone(),
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/crates/fluss/src/metadata/database.rs
b/crates/fluss/src/metadata/database.rs
index 15fefb54..76fe5114 100644
--- a/crates/fluss/src/metadata/database.rs
+++ b/crates/fluss/src/metadata/database.rs
@@ -18,6 +18,7 @@
use crate::error::Error::JsonSerdeError;
use crate::error::Result;
use crate::metadata::JsonSerde;
+use crate::proto::PbDatabaseSummary;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::collections::HashMap;
@@ -205,6 +206,24 @@ impl DatabaseDescriptor {
}
}
+/// Lightweight summary of a database returned by `list_database_summaries`.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct DatabaseSummary {
+ pub database_name: String,
+ pub created_time: i64,
+ pub table_count: i32,
+}
+
+impl DatabaseSummary {
+ pub fn from_pb(pb: &PbDatabaseSummary) -> Self {
+ Self {
+ database_name: pb.database_name.clone(),
+ created_time: pb.created_time,
+ table_count: pb.table_count,
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/crates/fluss/src/metadata/kv_snapshot.rs
b/crates/fluss/src/metadata/kv_snapshot.rs
new file mode 100644
index 00000000..054254ad
--- /dev/null
+++ b/crates/fluss/src/metadata/kv_snapshot.rs
@@ -0,0 +1,171 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::proto::{
+ AcquireKvSnapshotLeaseResponse, GetKvSnapshotMetadataResponse,
GetLatestKvSnapshotsResponse,
+ ListKvSnapshotsResponse, PbKvSnapshot, PbRemotePathAndLocalFile,
+};
+use crate::{BucketId, PartitionId, TableId};
+
+use crate::metadata::KvSnapshotLeaseForTable;
+
+/// Per-bucket KV snapshot info.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct KvSnapshot {
+ pub bucket_id: BucketId,
+ pub snapshot_id: Option<i64>,
+ pub log_offset: Option<i64>,
+}
+
+impl KvSnapshot {
+ pub fn from_pb(pb: &PbKvSnapshot) -> Self {
+ Self {
+ bucket_id: pb.bucket_id,
+ snapshot_id: pb.snapshot_id,
+ log_offset: pb.log_offset,
+ }
+ }
+}
+
+/// Result of `get_latest_kv_snapshots`.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct LatestKvSnapshots {
+ pub table_id: TableId,
+ pub partition_id: Option<PartitionId>,
+ pub latest_snapshots: Vec<KvSnapshot>,
+}
+
+impl LatestKvSnapshots {
+ pub fn from_pb(pb: &GetLatestKvSnapshotsResponse) -> Self {
+ Self {
+ table_id: pb.table_id,
+ partition_id: pb.partition_id,
+ latest_snapshots: pb
+ .latest_snapshots
+ .iter()
+ .map(KvSnapshot::from_pb)
+ .collect(),
+ }
+ }
+}
+
+/// One file in a KV snapshot manifest: its remote path and the local filename
+/// the server expects clients to materialize it as.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RemotePathAndLocalFile {
+ pub remote_path: String,
+ pub local_file_name: String,
+}
+
+impl RemotePathAndLocalFile {
+ pub fn from_pb(pb: &PbRemotePathAndLocalFile) -> Self {
+ Self {
+ remote_path: pb.remote_path.clone(),
+ local_file_name: pb.local_file_name.clone(),
+ }
+ }
+}
+
+/// Result of `get_kv_snapshot_metadata`.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct KvSnapshotMetadata {
+ pub log_offset: i64,
+ pub snapshot_files: Vec<RemotePathAndLocalFile>,
+}
+
+impl KvSnapshotMetadata {
+ pub fn from_pb(pb: &GetKvSnapshotMetadataResponse) -> Self {
+ Self {
+ log_offset: pb.log_offset,
+ snapshot_files: pb
+ .snapshot_files
+ .iter()
+ .map(RemotePathAndLocalFile::from_pb)
+ .collect(),
+ }
+ }
+}
+
+/// Result of `acquire_kv_snapshot_lease` — any snapshots the server could not
+/// lease (typically because they were evicted concurrently).
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct AcquireKvSnapshotLeaseResult {
+ pub unavailable_snapshots: Vec<KvSnapshotLeaseForTable>,
+}
+
+impl AcquireKvSnapshotLeaseResult {
+ pub fn from_pb(pb: &AcquireKvSnapshotLeaseResponse) -> Self {
+ Self {
+ unavailable_snapshots: pb
+ .unavailable_snapshots
+ .iter()
+ .map(KvSnapshotLeaseForTable::from_pb)
+ .collect(),
+ }
+ }
+}
+
+/// Result of `list_kv_snapshots`.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ActiveKvSnapshots {
+ pub table_id: TableId,
+ pub partition_id: Option<PartitionId>,
+ pub active_snapshots: Vec<KvSnapshot>,
+}
+
+impl ActiveKvSnapshots {
+ pub fn from_pb(pb: &ListKvSnapshotsResponse) -> Self {
+ Self {
+ table_id: pb.table_id,
+ partition_id: pb.partition_id,
+ active_snapshots: pb
+ .active_snapshots
+ .iter()
+ .map(KvSnapshot::from_pb)
+ .collect(),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_kv_snapshot_from_pb() {
+ let pb = PbKvSnapshot {
+ bucket_id: 3,
+ snapshot_id: Some(7),
+ log_offset: Some(42),
+ };
+ let s = KvSnapshot::from_pb(&pb);
+ assert_eq!(s.bucket_id, 3);
+ assert_eq!(s.snapshot_id, Some(7));
+ assert_eq!(s.log_offset, Some(42));
+ }
+
+ #[test]
+ fn test_remote_path_and_local_file_from_pb() {
+ let pb = PbRemotePathAndLocalFile {
+ remote_path: "s3://bucket/snap/1.sst".to_string(),
+ local_file_name: "1.sst".to_string(),
+ };
+ let f = RemotePathAndLocalFile::from_pb(&pb);
+ assert_eq!(f.remote_path, "s3://bucket/snap/1.sst");
+ assert_eq!(f.local_file_name, "1.sst");
+ }
+}
diff --git a/crates/fluss/src/metadata/kv_snapshot_lease.rs
b/crates/fluss/src/metadata/kv_snapshot_lease.rs
index 22679136..98638f5d 100644
--- a/crates/fluss/src/metadata/kv_snapshot_lease.rs
+++ b/crates/fluss/src/metadata/kv_snapshot_lease.rs
@@ -16,12 +16,13 @@
// under the License.
use crate::proto::{PbKvSnapshotLeaseForBucket, PbKvSnapshotLeaseForTable};
+use crate::{BucketId, PartitionId, TableId};
/// One bucket's slot in a KV-snapshot lease request.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KvSnapshotLeaseForBucket {
- pub partition_id: Option<i64>,
- pub bucket_id: i32,
+ pub partition_id: Option<PartitionId>,
+ pub bucket_id: BucketId,
pub snapshot_id: i64,
}
@@ -46,7 +47,7 @@ impl KvSnapshotLeaseForBucket {
/// All the buckets of a single table that should be leased together.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KvSnapshotLeaseForTable {
- pub table_id: i64,
+ pub table_id: TableId,
pub bucket_snapshots: Vec<KvSnapshotLeaseForBucket>,
}
diff --git a/crates/fluss/src/metadata/lake_snapshot.rs
b/crates/fluss/src/metadata/lake_snapshot.rs
new file mode 100644
index 00000000..5ff50ad6
--- /dev/null
+++ b/crates/fluss/src/metadata/lake_snapshot.rs
@@ -0,0 +1,83 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::proto::{GetLakeSnapshotResponse, PbLakeSnapshotForBucket};
+use crate::{BucketId, PartitionId, TableId};
+
+/// One bucket's slice of a lake snapshot.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct LakeBucketSnapshot {
+ pub partition_id: Option<PartitionId>,
+ pub bucket_id: BucketId,
+ pub log_offset: Option<i64>,
+ pub partition_name: Option<String>,
+}
+
+impl LakeBucketSnapshot {
+ pub fn from_pb(pb: &PbLakeSnapshotForBucket) -> Self {
+ Self {
+ partition_id: pb.partition_id,
+ bucket_id: pb.bucket_id,
+ log_offset: pb.log_offset,
+ partition_name: pb.partition_name.clone(),
+ }
+ }
+}
+
+/// Result of `get_lake_snapshot` — a specific snapshot's bucket layout.
+/// (Distinct from [`LakeSnapshot`](super::LakeSnapshot), which represents the
+/// "latest" snapshot summary returned by `get_latest_lake_snapshot`.)
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct LakeSnapshotInfo {
+ pub table_id: TableId,
+ pub snapshot_id: i64,
+ pub bucket_snapshots: Vec<LakeBucketSnapshot>,
+}
+
+impl LakeSnapshotInfo {
+ pub fn from_pb(pb: &GetLakeSnapshotResponse) -> Self {
+ Self {
+ table_id: pb.table_id,
+ snapshot_id: pb.snapshot_id,
+ bucket_snapshots: pb
+ .bucket_snapshots
+ .iter()
+ .map(LakeBucketSnapshot::from_pb)
+ .collect(),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_lake_bucket_snapshot_from_pb() {
+ let pb = PbLakeSnapshotForBucket {
+ partition_id: Some(1),
+ bucket_id: 2,
+ log_offset: Some(3),
+ partition_name: Some("date=2024-01-01".to_string()),
+ };
+ let s = LakeBucketSnapshot::from_pb(&pb);
+ assert_eq!(s.bucket_id, 2);
+ assert_eq!(s.partition_id, Some(1));
+ assert_eq!(s.log_offset, Some(3));
+ assert_eq!(s.partition_name.as_deref(), Some("date=2024-01-01"));
+ }
+}
diff --git a/crates/fluss/src/metadata/mod.rs b/crates/fluss/src/metadata/mod.rs
index 0249fefe..d992273c 100644
--- a/crates/fluss/src/metadata/mod.rs
+++ b/crates/fluss/src/metadata/mod.rs
@@ -16,15 +16,21 @@
// under the License.
mod acl;
+mod cluster_health;
mod config;
mod data_lake_format;
mod database;
mod datatype;
mod goal_type;
mod json_serde;
+mod kv_snapshot;
mod kv_snapshot_lease;
+mod lake_snapshot;
mod partition;
mod producer_offsets;
+mod rebalance;
+mod register_producer_result;
+mod remote_log;
mod schema_util;
mod server_tag;
mod table;
@@ -32,15 +38,21 @@ mod table_change;
mod table_stats;
pub use acl::*;
+pub use cluster_health::*;
pub use config::*;
pub use data_lake_format::*;
pub use database::*;
pub use datatype::*;
pub use goal_type::*;
pub use json_serde::*;
+pub use kv_snapshot::*;
pub use kv_snapshot_lease::*;
+pub use lake_snapshot::*;
pub use partition::*;
pub use producer_offsets::*;
+pub use rebalance::*;
+pub use register_producer_result::*;
+pub use remote_log::*;
pub(crate) use schema_util::{UNEXIST_MAPPING, index_mapping};
pub use server_tag::*;
pub use table::*;
diff --git a/crates/fluss/src/metadata/producer_offsets.rs
b/crates/fluss/src/metadata/producer_offsets.rs
index f0daddbc..f367c390 100644
--- a/crates/fluss/src/metadata/producer_offsets.rs
+++ b/crates/fluss/src/metadata/producer_offsets.rs
@@ -15,13 +15,14 @@
// specific language governing permissions and limitations
// under the License.
-use crate::proto::{PbBucketOffset, PbProducerTableOffsets};
+use crate::proto::{GetProducerOffsetsResponse, PbBucketOffset,
PbProducerTableOffsets};
+use crate::{BucketId, PartitionId, TableId};
/// Per-bucket producer log-end offset.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BucketOffset {
- pub partition_id: Option<i64>,
- pub bucket_id: i32,
+ pub partition_id: Option<PartitionId>,
+ pub bucket_id: BucketId,
pub log_end_offset: Option<i64>,
}
@@ -46,7 +47,7 @@ impl BucketOffset {
/// All bucket offsets of a single table belonging to one producer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProducerTableOffsets {
- pub table_id: i64,
+ pub table_id: TableId,
pub bucket_offsets: Vec<BucketOffset>,
}
@@ -74,6 +75,28 @@ impl ProducerTableOffsets {
}
}
+/// Result of `get_producer_offsets`.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ProducerOffsets {
+ pub producer_id: Option<String>,
+ pub expiration_time: Option<i64>,
+ pub table_offsets: Vec<ProducerTableOffsets>,
+}
+
+impl ProducerOffsets {
+ pub fn from_pb(pb: &GetProducerOffsetsResponse) -> Self {
+ Self {
+ producer_id: pb.producer_id.clone(),
+ expiration_time: pb.expiration_time,
+ table_offsets: pb
+ .table_offsets
+ .iter()
+ .map(ProducerTableOffsets::from_pb)
+ .collect(),
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/crates/fluss/src/metadata/rebalance.rs
b/crates/fluss/src/metadata/rebalance.rs
new file mode 100644
index 00000000..13826f9e
--- /dev/null
+++ b/crates/fluss/src/metadata/rebalance.rs
@@ -0,0 +1,187 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::error::{Error, Result};
+use crate::proto::{
+ ListRebalanceProgressResponse, PbRebalancePlanForBucket,
PbRebalanceProgressForBucket,
+ PbRebalanceProgressForTable,
+};
+use crate::{BucketId, PartitionId, TableId};
+
+/// Mirrors Java `org.apache.fluss.cluster.rebalance.RebalanceStatus`.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RebalanceStatus {
+ NotStarted,
+ Rebalancing,
+ Failed,
+ Completed,
+ Canceled,
+ Timeout,
+}
+
+impl RebalanceStatus {
+ pub fn to_i32(self) -> i32 {
+ match self {
+ Self::NotStarted => 0,
+ Self::Rebalancing => 1,
+ Self::Failed => 2,
+ Self::Completed => 3,
+ Self::Canceled => 4,
+ Self::Timeout => 5,
+ }
+ }
+
+ pub fn try_from_i32(value: i32) -> Result<Self> {
+ match value {
+ 0 => Ok(Self::NotStarted),
+ 1 => Ok(Self::Rebalancing),
+ 2 => Ok(Self::Failed),
+ 3 => Ok(Self::Completed),
+ 4 => Ok(Self::Canceled),
+ 5 => Ok(Self::Timeout),
+ _ => Err(Error::IllegalArgument {
+ message: format!("Unsupported RebalanceStatus: {value}"),
+ }),
+ }
+ }
+}
+
+/// Per-bucket plan in a rebalance: who the leader was and who it will be, who
+/// the replicas were and who they will be.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct BucketRebalancePlan {
+ pub partition_id: Option<PartitionId>,
+ pub bucket_id: BucketId,
+ pub original_leader: Option<i32>,
+ pub new_leader: Option<i32>,
+ pub original_replicas: Vec<i32>,
+ pub new_replicas: Vec<i32>,
+}
+
+impl BucketRebalancePlan {
+ pub fn from_pb(pb: &PbRebalancePlanForBucket) -> Self {
+ Self {
+ partition_id: pb.partition_id,
+ bucket_id: pb.bucket_id,
+ original_leader: pb.original_leader,
+ new_leader: pb.new_leader,
+ original_replicas: pb.original_replicas.clone(),
+ new_replicas: pb.new_replicas.clone(),
+ }
+ }
+}
+
+/// Per-bucket rebalance progress: the planned move and its current status
code.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct BucketRebalanceProgress {
+ pub rebalance_plan: BucketRebalancePlan,
+ pub rebalance_status: RebalanceStatus,
+}
+
+impl BucketRebalanceProgress {
+ pub fn from_pb(pb: &PbRebalanceProgressForBucket) -> Result<Self> {
+ Ok(Self {
+ rebalance_plan: BucketRebalancePlan::from_pb(&pb.rebalance_plan),
+ rebalance_status:
RebalanceStatus::try_from_i32(pb.rebalance_status)?,
+ })
+ }
+}
+
+/// All bucket progress for one table.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct TableRebalanceProgress {
+ pub table_id: TableId,
+ pub buckets_progress: Vec<BucketRebalanceProgress>,
+}
+
+impl TableRebalanceProgress {
+ pub fn from_pb(pb: &PbRebalanceProgressForTable) -> Result<Self> {
+ Ok(Self {
+ table_id: pb.table_id,
+ buckets_progress: pb
+ .buckets_progress
+ .iter()
+ .map(BucketRebalanceProgress::from_pb)
+ .collect::<Result<Vec<_>>>()?,
+ })
+ }
+}
+
+/// Result of `list_rebalance_progress`.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RebalanceProgress {
+ pub rebalance_id: Option<String>,
+ pub rebalance_status: Option<RebalanceStatus>,
+ pub table_progress: Vec<TableRebalanceProgress>,
+}
+
+impl RebalanceProgress {
+ pub fn from_pb(pb: &ListRebalanceProgressResponse) -> Result<Self> {
+ Ok(Self {
+ rebalance_id: pb.rebalance_id.clone(),
+ rebalance_status: pb
+ .rebalance_status
+ .map(RebalanceStatus::try_from_i32)
+ .transpose()?,
+ table_progress: pb
+ .table_progress
+ .iter()
+ .map(TableRebalanceProgress::from_pb)
+ .collect::<Result<Vec<_>>>()?,
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_rebalance_status_roundtrip() {
+ for s in [
+ RebalanceStatus::NotStarted,
+ RebalanceStatus::Rebalancing,
+ RebalanceStatus::Failed,
+ RebalanceStatus::Completed,
+ RebalanceStatus::Canceled,
+ RebalanceStatus::Timeout,
+ ] {
+ assert_eq!(RebalanceStatus::try_from_i32(s.to_i32()).unwrap(), s);
+ }
+ }
+
+ #[test]
+ fn test_rebalance_status_unknown() {
+ assert!(RebalanceStatus::try_from_i32(99).is_err());
+ }
+
+ #[test]
+ fn test_bucket_rebalance_plan_from_pb() {
+ let pb = PbRebalancePlanForBucket {
+ partition_id: Some(1),
+ bucket_id: 2,
+ original_leader: Some(3),
+ new_leader: Some(4),
+ original_replicas: vec![3, 5, 6],
+ new_replicas: vec![4, 5, 6],
+ };
+ let p = BucketRebalancePlan::from_pb(&pb);
+ assert_eq!(p.bucket_id, 2);
+ assert_eq!(p.new_leader, Some(4));
+ assert_eq!(p.new_replicas, vec![4, 5, 6]);
+ }
+}
diff --git a/crates/fluss/src/metadata/register_producer_result.rs
b/crates/fluss/src/metadata/register_producer_result.rs
new file mode 100644
index 00000000..1c0c8e50
--- /dev/null
+++ b/crates/fluss/src/metadata/register_producer_result.rs
@@ -0,0 +1,66 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::error::{Error, Result};
+
+/// Mirrors Java `org.apache.fluss.client.admin.RegisterResult`.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RegisterProducerResult {
+ /// Snapshot was newly created (first-startup scenario; no undo recovery
needed).
+ Created,
+ /// Snapshot already existed (failover scenario; caller should perform
undo recovery).
+ AlreadyExists,
+}
+
+impl RegisterProducerResult {
+ pub fn to_i32(self) -> i32 {
+ match self {
+ Self::Created => 0,
+ Self::AlreadyExists => 1,
+ }
+ }
+
+ pub fn try_from_i32(value: i32) -> Result<Self> {
+ match value {
+ 0 => Ok(Self::Created),
+ 1 => Ok(Self::AlreadyExists),
+ _ => Err(Error::IllegalArgument {
+ message: format!("Unsupported RegisterProducerResult:
{value}"),
+ }),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_register_producer_result_roundtrip() {
+ for r in [
+ RegisterProducerResult::Created,
+ RegisterProducerResult::AlreadyExists,
+ ] {
+
assert_eq!(RegisterProducerResult::try_from_i32(r.to_i32()).unwrap(), r);
+ }
+ }
+
+ #[test]
+ fn test_register_producer_result_unknown() {
+ assert!(RegisterProducerResult::try_from_i32(99).is_err());
+ }
+}
diff --git a/crates/fluss/src/metadata/remote_log.rs
b/crates/fluss/src/metadata/remote_log.rs
new file mode 100644
index 00000000..d3fd3e33
--- /dev/null
+++ b/crates/fluss/src/metadata/remote_log.rs
@@ -0,0 +1,59 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::metadata::TableBucket;
+use crate::proto::PbRemoteLogManifestEntry;
+
+/// One bucket's remote-log manifest pointer.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RemoteLogManifestEntry {
+ pub table_bucket: TableBucket,
+ pub remote_log_manifest_path: String,
+ pub remote_log_end_offset: i64,
+}
+
+impl RemoteLogManifestEntry {
+ pub fn from_pb(pb: &PbRemoteLogManifestEntry) -> Self {
+ Self {
+ table_bucket: TableBucket::from_pb(&pb.table_bucket),
+ remote_log_manifest_path: pb.remote_log_manifest_path.clone(),
+ remote_log_end_offset: pb.remote_log_end_offset,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::proto::PbTableBucket;
+
+ #[test]
+ fn test_remote_log_manifest_entry_from_pb() {
+ let pb = PbRemoteLogManifestEntry {
+ table_bucket: PbTableBucket {
+ table_id: 1,
+ partition_id: None,
+ bucket_id: 2,
+ },
+ remote_log_manifest_path: "s3://bucket/manifest.json".to_string(),
+ remote_log_end_offset: 999,
+ };
+ let m = RemoteLogManifestEntry::from_pb(&pb);
+ assert_eq!(m.remote_log_end_offset, 999);
+ assert_eq!(m.table_bucket.bucket_id(), 2);
+ }
+}
diff --git a/crates/fluss/src/metadata/table_change.rs
b/crates/fluss/src/metadata/table_change.rs
index 2ecf6cfa..3e5866a2 100644
--- a/crates/fluss/src/metadata/table_change.rs
+++ b/crates/fluss/src/metadata/table_change.rs
@@ -15,6 +15,7 @@
// specific language governing permissions and limitations
// under the License.
+use super::AlterConfig;
use crate::error::{Error, Result};
use crate::proto::{PbAddColumn, PbDropColumn, PbModifyColumn, PbRenameColumn};
@@ -121,6 +122,18 @@ impl RenameColumn {
}
}
+/// Bundle of column-level changes for a single `alter_table` call. Empty
`Vec`s
+/// mean "no change of that kind"; pass `Default::default()` to send only
+/// config changes.
+#[derive(Debug, Default, Clone, PartialEq, Eq)]
+pub struct AlterTableChanges {
+ pub config_changes: Vec<AlterConfig>,
+ pub add_columns: Vec<AddColumn>,
+ pub drop_columns: Vec<DropColumn>,
+ pub rename_columns: Vec<RenameColumn>,
+ pub modify_columns: Vec<ModifyColumn>,
+}
+
/// Modify a column's type/comment/position. Mirrors the `ModifyColumn`
variant of
/// Java `TableChange`. All fields except `column_name` are optional — only the
/// non-`None` ones are applied.
diff --git a/crates/fluss/src/metadata/table_stats.rs
b/crates/fluss/src/metadata/table_stats.rs
index ad2d1fc2..53c6f72f 100644
--- a/crates/fluss/src/metadata/table_stats.rs
+++ b/crates/fluss/src/metadata/table_stats.rs
@@ -15,18 +15,19 @@
// specific language governing permissions and limitations
// under the License.
-use crate::proto::PbTableStatsReqForBucket;
+use crate::proto::{GetTableStatsResponse, PbTableStatsReqForBucket,
PbTableStatsRespForBucket};
+use crate::{BucketId, PartitionId};
/// Per-bucket request item for `GetTableStats`.
/// Mirrors the bucket-stats request shape used by the Java client.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BucketStatsRequest {
- pub partition_id: Option<i64>,
- pub bucket_id: i32,
+ pub partition_id: Option<PartitionId>,
+ pub bucket_id: BucketId,
}
impl BucketStatsRequest {
- pub fn new(partition_id: Option<i64>, bucket_id: i32) -> Self {
+ pub fn new(partition_id: Option<PartitionId>, bucket_id: BucketId) -> Self
{
Self {
partition_id,
bucket_id,
@@ -48,6 +49,51 @@ impl BucketStatsRequest {
}
}
+/// Per-bucket stats result returned by `GetTableStats`. `row_count` is `None`
+/// when the server returned an error for the bucket; check `error` in that
case.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct BucketStats {
+ pub bucket_id: BucketId,
+ pub partition_id: Option<PartitionId>,
+ pub row_count: Option<i64>,
+ pub error: Option<BucketStatsError>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct BucketStatsError {
+ pub code: i32,
+ pub message: Option<String>,
+}
+
+impl BucketStats {
+ pub fn from_pb(pb: &PbTableStatsRespForBucket) -> Self {
+ let error = pb.error_code.map(|code| BucketStatsError {
+ code,
+ message: pb.error_message.clone(),
+ });
+ Self {
+ bucket_id: pb.bucket_id,
+ partition_id: pb.partition_id,
+ row_count: pb.row_count,
+ error,
+ }
+ }
+}
+
+/// Full result of `GetTableStats` — one entry per requested bucket.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct TableStats {
+ pub buckets: Vec<BucketStats>,
+}
+
+impl TableStats {
+ pub fn from_pb(pb: &GetTableStatsResponse) -> Self {
+ Self {
+ buckets:
pb.buckets_resp.iter().map(BucketStats::from_pb).collect(),
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -62,4 +108,33 @@ mod tests {
assert_eq!(BucketStatsRequest::from_pb(&pb), req);
}
}
+
+ #[test]
+ fn test_bucket_stats_from_pb_ok() {
+ let pb = PbTableStatsRespForBucket {
+ error_code: None,
+ error_message: None,
+ partition_id: Some(1),
+ bucket_id: 7,
+ row_count: Some(123),
+ };
+ let s = BucketStats::from_pb(&pb);
+ assert_eq!(s.bucket_id, 7);
+ assert_eq!(s.row_count, Some(123));
+ assert!(s.error.is_none());
+ }
+
+ #[test]
+ fn test_bucket_stats_from_pb_err() {
+ let pb = PbTableStatsRespForBucket {
+ error_code: Some(7),
+ error_message: Some("nope".to_string()),
+ partition_id: None,
+ bucket_id: 2,
+ row_count: None,
+ };
+ let s = BucketStats::from_pb(&pb);
+ assert_eq!(s.error.as_ref().unwrap().code, 7);
+ assert_eq!(s.error.as_ref().unwrap().message.as_deref(), Some("nope"));
+ }
}
diff --git a/crates/fluss/src/rpc/message/alter_database.rs
b/crates/fluss/src/rpc/message/alter_database.rs
index 3eb08163..2135e663 100644
--- a/crates/fluss/src/rpc/message/alter_database.rs
+++ b/crates/fluss/src/rpc/message/alter_database.rs
@@ -33,13 +33,14 @@ impl AlterDatabaseRequest {
database_name: &str,
ignore_if_not_exists: bool,
config_changes: Vec<AlterConfig>,
+ comment: Option<&str>,
) -> Self {
AlterDatabaseRequest {
inner_request: proto::AlterDatabaseRequest {
database_name: database_name.to_string(),
ignore_if_not_exists,
config_changes:
config_changes.iter().map(AlterConfig::to_pb).collect(),
- comment: None,
+ comment: comment.map(str::to_string),
},
}
}