redis_enterprise/bdb.rs
1//! Database (BDB) management for Redis Enterprise
2//!
3//! ## Overview
4//! - Create, list, update, and delete databases
5//! - Execute database actions (backup, restore, import, export)
6//! - Monitor database status and metrics
7//! - Configure database endpoints and sharding
8//!
9//! ## Examples
10//!
11//! ### Creating a Database
12//! ```no_run
13//! use redis_enterprise::{EnterpriseClient, CreateDatabaseRequest};
14//!
15//! # async fn example(client: EnterpriseClient) -> Result<(), Box<dyn std::error::Error>> {
16//! // Simple cache database
17//! let cache_db = CreateDatabaseRequest::builder()
18//! .name("my-cache")
19//! .memory_size(1_073_741_824) // 1GB
20//! .eviction_policy("allkeys-lru")
21//! .persistence("disabled")
22//! .build();
23//!
24//! let db = client.databases().create(cache_db).await?;
25//! println!("Created database with ID: {}", db.uid);
26//! # Ok(())
27//! # }
28//! ```
29//!
30//! ### Database Actions
31//! ```no_run
32//! # use redis_enterprise::EnterpriseClient;
33//! # async fn example(client: EnterpriseClient) -> Result<(), Box<dyn std::error::Error>> {
34//! let db_id = 1;
35//!
36//! // Export to remote location
37//! let export = client.databases().export(db_id, "ftp://backup.site/db.rdb").await?;
38//! println!("Export initiated: {:?}", export.action_uid);
39//!
40//! // Import from backup
41//! let import = client.databases().import(db_id, "ftp://backup.site/db.rdb", true).await?;
42//! println!("Import started: {:?}", import.action_uid);
43//! # Ok(())
44//! # }
45//! ```
46//!
47//! ### Monitoring Databases
48//! ```no_run
49//! # use redis_enterprise::EnterpriseClient;
50//! # async fn example(client: EnterpriseClient) -> Result<(), Box<dyn std::error::Error>> {
51//! // List all databases
52//! let databases = client.databases().list().await?;
53//! for db in databases {
54//! println!("{}: {} MB used", db.name, db.memory_used.unwrap_or(0) / 1_048_576);
55//! }
56//!
57//! // Get database endpoints
58//! let endpoints = client.databases().endpoints(1).await?;
59//! for endpoint in endpoints {
60//! println!("Endpoint: {:?}:{:?}", endpoint.dns_name, endpoint.port);
61//! }
62//! # Ok(())
63//! # }
64//! ```
65
66use crate::client::RestClient;
67use crate::error::Result;
68use futures::stream::Stream;
69use serde::{Deserialize, Serialize};
70use serde_json::Value;
71use std::collections::BTreeMap;
72use std::pin::Pin;
73use std::time::Duration;
74use tokio::time::sleep;
75use typed_builder::TypedBuilder;
76
77// Aliases for easier use
78/// Alias for [`DatabaseInfo`]; the BDB ("Berkeley DB") naming is legacy Redis Enterprise terminology.
79pub type Database = DatabaseInfo;
80/// Alias for [`DatabaseHandler`]; retained for backwards compatibility.
81pub type BdbHandler = DatabaseHandler;
82/// Stream of database state updates yielded by the database watcher.
83pub type DatabaseWatchStream<'a> =
84 Pin<Box<dyn Stream<Item = Result<(DatabaseInfo, Option<String>)>> + Send + 'a>>;
85
86/// Response from database action operations
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct DatabaseActionResponse {
89 /// The action UID for tracking async operations
90 pub action_uid: String,
91 /// Description of the action
92 pub description: Option<String>,
93}
94
95/// Response from import operation
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct ImportResponse {
98 /// The action UID for tracking the import operation
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub action_uid: Option<String>,
101 /// Import status
102 pub status: Option<String>,
103}
104
105/// Response from export operation
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ExportResponse {
108 /// The action UID for tracking the export operation
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub action_uid: Option<String>,
111 /// Export status
112 pub status: Option<String>,
113}
114
115/// Module information for database upgrade
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct ModuleUpgrade {
118 /// Module name
119 pub module_name: String,
120 /// Module version
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub new_version: Option<String>,
123 /// Module arguments
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub module_args: Option<String>,
126}
127
128/// Request for database upgrade operation
129///
130/// # Examples
131///
132/// ```rust,no_run
133/// use redis_enterprise::bdb::DatabaseUpgradeRequest;
134///
135/// // Upgrade to latest Redis version with role preservation
136/// let request = DatabaseUpgradeRequest::builder()
137/// .preserve_roles(true)
138/// .build();
139///
140/// // Upgrade to specific version
141/// let request = DatabaseUpgradeRequest::builder()
142/// .redis_version("7.4.2")
143/// .preserve_roles(true)
144/// .parallel_shards_upgrade(2)
145/// .build();
146/// ```
147#[derive(Debug, Clone, Serialize, Deserialize, Default, TypedBuilder)]
148pub struct DatabaseUpgradeRequest {
149 /// Target Redis version (optional, defaults to latest)
150 #[serde(skip_serializing_if = "Option::is_none")]
151 #[builder(default, setter(into, strip_option))]
152 pub redis_version: Option<String>,
153
154 /// Preserve master/replica roles (requires extra failover)
155 #[serde(skip_serializing_if = "Option::is_none")]
156 #[builder(default, setter(strip_option))]
157 pub preserve_roles: Option<bool>,
158
159 /// Restart shards even if no version change
160 #[serde(skip_serializing_if = "Option::is_none")]
161 #[builder(default, setter(strip_option))]
162 pub force_restart: Option<bool>,
163
164 /// Allow data loss in non-replicated, non-persistent databases
165 #[serde(skip_serializing_if = "Option::is_none")]
166 #[builder(default, setter(strip_option))]
167 pub may_discard_data: Option<bool>,
168
169 /// Force data discard even if replicated/persistent
170 #[serde(skip_serializing_if = "Option::is_none")]
171 #[builder(default, setter(strip_option))]
172 pub force_discard: Option<bool>,
173
174 /// Keep current CRDT protocol version
175 #[serde(skip_serializing_if = "Option::is_none")]
176 #[builder(default, setter(strip_option))]
177 pub keep_crdt_protocol_version: Option<bool>,
178
179 /// Maximum parallel shard upgrades (default: all shards)
180 #[serde(skip_serializing_if = "Option::is_none")]
181 #[builder(default, setter(strip_option))]
182 pub parallel_shards_upgrade: Option<u32>,
183
184 /// Modules to upgrade alongside Redis
185 #[serde(skip_serializing_if = "Option::is_none")]
186 #[builder(default, setter(strip_option))]
187 pub modules: Option<Vec<ModuleUpgrade>>,
188}
189
190/// Version-aware database information from the REST API.
191#[derive(Debug, Clone, Serialize, Deserialize)]
192#[non_exhaustive]
193pub struct DatabaseInfo {
194 // Core database identification and status
195 /// Database's unique ID (read-only).
196 pub uid: u32,
197 /// Database name.
198 pub name: String,
199 /// TCP port on which the database is available (read-only).
200 pub port: Option<u16>,
201 /// Current status of the database (e.g. `"active"`, `"pending"`).
202 pub status: Option<String>,
203 /// Database memory limit in bytes (0 for unlimited).
204 pub memory_size: Option<u64>,
205 /// Current memory usage in bytes (read-only).
206 pub memory_used: Option<u64>,
207
208 /// Database type (e.g., "redis", "memcached")
209 #[serde(rename = "type")]
210 pub type_: Option<String>,
211 /// Database version (read-only).
212 pub version: Option<String>,
213
214 /// Account and action tracking
215 pub account_id: Option<u32>,
216 /// UID of the most recent action affecting this database (read-only).
217 pub action_uid: Option<String>,
218
219 // Sharding and placement
220 /// Number of database shards.
221 pub shards_count: Option<u32>,
222 /// List of shard UIDs that compose the database.
223 pub shard_list: Option<Vec<u32>>,
224 /// Whether the database is sharded.
225 pub sharding: Option<bool>,
226 /// Shard placement strategy (e.g. `"dense"`, `"sparse"`).
227 pub shards_placement: Option<String>,
228 /// Whether in-memory replication is enabled.
229 pub replication: Option<bool>,
230
231 // Endpoints and networking
232 /// Endpoints exposed by this database. See [`EndpointInfo`].
233 pub endpoints: Option<Vec<EndpointInfo>>,
234 /// Primary endpoint address (read-only).
235 pub endpoint: Option<String>,
236 /// List of endpoint IP addresses (read-only).
237 pub endpoint_ip: Option<Vec<String>>,
238 /// Node UID that currently hosts the endpoint (read-only).
239 pub endpoint_node: Option<u32>,
240 /// DNS name pointing to the master endpoint (read-only).
241 pub dns_address_master: Option<String>,
242
243 // Data persistence and backup
244 /// Persistence policy (e.g. `"disabled"`, `"aof"`, `"snapshot"`).
245 pub persistence: Option<String>,
246 /// Data persistence mode (alias for `persistence` on some API versions).
247 pub data_persistence: Option<String>,
248 /// Eviction policy when the database reaches its memory limit (e.g. `"allkeys-lru"`).
249 pub eviction_policy: Option<String>,
250
251 // Timestamps
252 /// Timestamp when the database was created (ISO-8601).
253 pub created_time: Option<String>,
254 /// Timestamp of the most recent configuration change (ISO-8601).
255 pub last_changed_time: Option<String>,
256 /// Timestamp of the most recent successful backup (ISO-8601).
257 pub last_backup_time: Option<String>,
258 /// Timestamp of the most recent successful export (ISO-8601).
259 pub last_export_time: Option<String>,
260
261 // Security and authentication
262 /// Whether weak hashing is permitted for mTLS connections.
263 pub mtls_allow_weak_hashing: Option<bool>,
264 /// Whether outdated/expired certificates are permitted for mTLS connections.
265 pub mtls_allow_outdated_certs: Option<bool>,
266 /// Redis password used for client authentication.
267 pub authentication_redis_pass: Option<String>,
268 /// Admin password used for elevated authentication.
269 pub authentication_admin_pass: Option<String>,
270 /// SASL password (Memcached databases).
271 pub authentication_sasl_pass: Option<String>,
272 /// SASL username (Memcached databases).
273 pub authentication_sasl_uname: Option<String>,
274 /// List of SSL client certificates accepted for authentication.
275 pub authentication_ssl_client_certs: Option<Vec<Value>>,
276 /// List of SSL certificates trusted for CRDT (Active-Active) connections.
277 pub authentication_ssl_crdt_certs: Option<Vec<Value>>,
278 /// List of authorized subjects for certificate-based authentication.
279 pub authorized_subjects: Option<Vec<Value>>,
280 /// Whether inter-node data encryption is enabled.
281 pub data_internode_encryption: Option<bool>,
282 /// Whether SSL/TLS is required for client connections.
283 pub ssl: Option<bool>,
284 /// TLS mode for client connections (e.g. `"enabled"`, `"disabled"`).
285 pub tls_mode: Option<String>,
286 /// Client certificate enforcement mode (e.g. `"enabled"`, `"disabled"`).
287 pub enforce_client_authentication: Option<String>,
288 /// Whether the default Redis user is enabled.
289 pub default_user: Option<bool>,
290 /// ACL configuration
291 pub acl: Option<Value>,
292 /// Client certificate subject validation type
293 pub client_cert_subject_validation_type: Option<String>,
294 /// Compare key hslot
295 pub compare_key_hslot: Option<bool>,
296 /// DNS suffixes for endpoints
297 pub dns_suffixes: Option<Vec<String>>,
298 /// Group UID for the database
299 pub group_uid: Option<u32>,
300 /// Redis cluster mode enabled
301 pub redis_cluster_enabled: Option<bool>,
302
303 // CRDT/Active-Active fields
304 /// Whether this database participates in a CRDT (Active-Active) deployment.
305 pub crdt: Option<bool>,
306 /// Whether CRDT (Active-Active) functionality is enabled.
307 pub crdt_enabled: Option<bool>,
308 /// CRDT configuration version.
309 pub crdt_config_version: Option<u32>,
310 /// Replica ID assigned to this database in the CRDT cluster.
311 pub crdt_replica_id: Option<u32>,
312 /// Comma-separated list of replica IDs that have been removed from the CRDT.
313 pub crdt_ghost_replica_ids: Option<String>,
314 /// CRDT feature-set version.
315 pub crdt_featureset_version: Option<u32>,
316 /// CRDT protocol version.
317 pub crdt_protocol_version: Option<u32>,
318 /// Globally unique identifier for the CRDT.
319 pub crdt_guid: Option<String>,
320 /// Modules configuration for the CRDT.
321 pub crdt_modules: Option<String>,
322 /// Comma-separated list of CRDT replica endpoints.
323 pub crdt_replicas: Option<String>,
324 /// List of CRDT replica sources.
325 pub crdt_sources: Option<Vec<Value>>,
326 /// CRDT sync state (e.g. `"enabled"`, `"disabled"`).
327 pub crdt_sync: Option<String>,
328 /// Seconds before a stalled CRDT sync connection raises an alarm.
329 pub crdt_sync_connection_alarm_timeout_seconds: Option<u32>,
330 /// Whether CRDT sync is distributed across shards.
331 pub crdt_sync_dist: Option<bool>,
332 /// Whether the CRDT syncer auto-unlatches on out-of-memory conditions.
333 pub crdt_syncer_auto_oom_unlatch: Option<bool>,
334 /// XADD stream ID uniqueness mode for CRDT.
335 pub crdt_xadd_id_uniqueness_mode: Option<String>,
336 /// Whether CRDT causal consistency is enabled.
337 pub crdt_causal_consistency: Option<bool>,
338 /// Replication backlog size for CRDT (bytes, or `"auto"`).
339 pub crdt_repl_backlog_size: Option<String>,
340
341 // Replication settings
342 /// Whether persistence is enabled on the master shard.
343 pub master_persistence: Option<bool>,
344 /// Whether slave (replica) high availability is enabled.
345 pub slave_ha: Option<bool>,
346 /// Priority for slave HA replica selection.
347 pub slave_ha_priority: Option<u32>,
348 /// Whether replica shards are read-only.
349 pub replica_read_only: Option<bool>,
350 /// List of upstream replica sources for Replica-Of mode.
351 pub replica_sources: Option<Vec<Value>>,
352 /// Replica sync state (e.g. `"enabled"`, `"paused"`).
353 pub replica_sync: Option<String>,
354 /// Seconds before a stalled replica sync connection raises an alarm.
355 pub replica_sync_connection_alarm_timeout_seconds: Option<u32>,
356 /// Whether replica sync is distributed across shards.
357 pub replica_sync_dist: Option<bool>,
358 /// Replication backlog size (bytes, or `"auto"`).
359 pub repl_backlog_size: Option<String>,
360
361 // Connection and performance settings
362 /// Maximum number of client connections (alias for `maxclients` on some versions).
363 pub max_connections: Option<u32>,
364 /// Maximum number of concurrent client connections.
365 pub maxclients: Option<u32>,
366 /// Number of proxy connection threads.
367 pub conns: Option<u32>,
368 /// Proxy connection type (e.g. `"per-thread"`, `"per-shard"`).
369 pub conns_type: Option<String>,
370 /// Maximum number of pipelined commands per client.
371 pub max_client_pipeline: Option<u32>,
372 /// Maximum number of pipelined commands.
373 pub max_pipelined: Option<u32>,
374
375 // AOF (Append Only File) settings
376 /// AOF (append-only file) fsync policy (e.g. `"appendfsync-every-sec"`).
377 pub aof_policy: Option<String>,
378 /// Maximum AOF file size in bytes.
379 pub max_aof_file_size: Option<u64>,
380 /// Maximum AOF load time in seconds.
381 pub max_aof_load_time: Option<u32>,
382
383 // Active defragmentation settings
384 /// Active defragmentation toggle (e.g. `"enabled"`, `"disabled"`).
385 pub activedefrag: Option<String>,
386 /// Maximum CPU percentage used by active defrag.
387 pub active_defrag_cycle_max: Option<u32>,
388 /// Minimum CPU percentage used by active defrag.
389 pub active_defrag_cycle_min: Option<u32>,
390 /// Minimum amount of fragmentation waste (bytes) before defrag starts.
391 pub active_defrag_ignore_bytes: Option<String>,
392 /// Maximum number of fields scanned per defrag cycle.
393 pub active_defrag_max_scan_fields: Option<u32>,
394 /// Lower fragmentation threshold (percent) for active defrag.
395 pub active_defrag_threshold_lower: Option<u32>,
396 /// Upper fragmentation threshold (percent) for active defrag.
397 pub active_defrag_threshold_upper: Option<u32>,
398
399 // Backup settings
400 /// Whether periodic backup is enabled.
401 pub backup: Option<bool>,
402 /// Reason for the most recent backup failure (read-only).
403 pub backup_failure_reason: Option<String>,
404 /// Number of backup snapshots retained.
405 pub backup_history: Option<u32>,
406 /// Backup interval in seconds.
407 pub backup_interval: Option<u32>,
408 /// Offset (in seconds) from midnight when scheduled backups run.
409 pub backup_interval_offset: Option<u32>,
410 /// Backup destination location (URI or storage config object).
411 pub backup_location: Option<Value>,
412 /// Percent progress (0–100) of the in-flight backup (read-only).
413 pub backup_progress: Option<f64>,
414 /// Current backup status (e.g. `"idle"`, `"running"`, `"failed"`).
415 pub backup_status: Option<String>,
416
417 // Import/Export settings
418 /// List of dataset import source descriptors.
419 pub dataset_import_sources: Option<Vec<Value>>,
420 /// Reason for the most recent import failure (read-only).
421 pub import_failure_reason: Option<String>,
422 /// Percent progress (0–100) of the in-flight import (read-only).
423 pub import_progress: Option<f64>,
424 /// Current import status (e.g. `"idle"`, `"running"`, `"failed"`).
425 pub import_status: Option<String>,
426 /// Reason for the most recent export failure (read-only).
427 pub export_failure_reason: Option<String>,
428 /// Percent progress (0–100) of the in-flight export (read-only).
429 pub export_progress: Option<f64>,
430 /// Current export status (e.g. `"idle"`, `"running"`, `"failed"`).
431 pub export_status: Option<String>,
432 /// Skip-analyze policy applied during import.
433 pub skip_import_analyze: Option<String>,
434
435 // Monitoring and metrics
436 /// Whether all metrics are exported.
437 pub metrics_export_all: Option<bool>,
438 /// Whether the text monitor log is generated for this database.
439 pub generate_text_monitor: Option<bool>,
440 /// Whether email alerts are enabled for this database.
441 pub email_alerts: Option<bool>,
442
443 // Modules and features
444 /// List of Redis modules loaded into the database.
445 pub module_list: Option<Vec<Value>>,
446 /// Search configuration - can be bool or object depending on API version
447 #[serde(default)]
448 pub search: Option<Value>,
449 /// Timeseries configuration - can be bool or object depending on API version
450 #[serde(default)]
451 pub timeseries: Option<Value>,
452
453 // BigStore/Flash storage settings
454 /// Whether Redis on Flash (BigStore) is enabled.
455 pub bigstore: Option<bool>,
456 /// RAM portion of the database (bytes) when BigStore is enabled.
457 pub bigstore_ram_size: Option<u64>,
458 /// Maximum percentage of memory used by RAM in BigStore.
459 pub bigstore_max_ram_ratio: Option<u32>,
460 /// Per-shard RAM weights for BigStore.
461 pub bigstore_ram_weights: Option<Vec<Value>>,
462 /// BigStore version in use.
463 pub bigstore_version: Option<u32>,
464
465 // Network and proxy settings
466 /// Proxy policy (e.g. `"single"`, `"all-master-shards"`, `"all-nodes"`).
467 pub proxy_policy: Option<String>,
468 /// Whether OSS-cluster API compatibility is enabled.
469 pub oss_cluster: Option<bool>,
470 /// Preferred endpoint type advertised by the OSS-cluster API (e.g. `"hostname"`, `"ip"`).
471 pub oss_cluster_api_preferred_endpoint_type: Option<String>,
472 /// Preferred IP type advertised by the OSS-cluster API (e.g. `"internal"`, `"external"`).
473 pub oss_cluster_api_preferred_ip_type: Option<String>,
474 /// Whether OSS-style hash-slot sharding is enabled.
475 pub oss_sharding: Option<bool>,
476
477 // Redis-specific settings
478 /// Redis Enterprise database server version (read-only).
479 pub redis_version: Option<String>,
480 /// Whether RESP3 protocol is enabled.
481 pub resp3: Option<bool>,
482 /// Comma-separated list of disabled Redis commands.
483 pub disabled_commands: Option<String>,
484
485 // Clustering and sharding
486 /// Hash-slot policy (e.g. `"legacy"`, `"16k"`).
487 pub hash_slots_policy: Option<String>,
488 /// List of regular expressions used to extract shard keys.
489 pub shard_key_regex: Option<Vec<Value>>,
490 /// Whether cross-slot multi-key commands are blocked.
491 pub shard_block_crossslot_keys: Option<bool>,
492 /// Whether commands referencing foreign keys are blocked.
493 pub shard_block_foreign_keys: Option<bool>,
494 /// Whether implicit shard keys are used.
495 pub implicit_shard_key: Option<bool>,
496
497 // Node placement and rack awareness
498 /// List of node UIDs to avoid when placing shards.
499 pub avoid_nodes: Option<Vec<String>>,
500 /// List of node UIDs preferred for placing shards.
501 pub use_nodes: Option<Vec<String>>,
502 /// Whether rack-aware shard placement is enabled.
503 pub rack_aware: Option<bool>,
504
505 // Operational settings
506 /// Whether automatic Redis upgrades are enabled.
507 pub auto_upgrade: Option<bool>,
508 /// Whether this is an internal/system database.
509 pub internal: Option<bool>,
510 /// Whether database connection auditing is enabled.
511 pub db_conns_auditing: Option<bool>,
512 /// Whether the replica flushes its dataset on full sync.
513 pub flush_on_fullsync: Option<bool>,
514 /// Whether selective flush is used for replica sync.
515 pub use_selective_flush: Option<bool>,
516
517 // Sync and replication control
518 /// Database sync mode.
519 pub sync: Option<String>,
520 /// List of sync sources for Replica-Of configurations.
521 pub sync_sources: Option<Vec<Value>>,
522 /// Number of dedicated threads used by the syncer.
523 pub sync_dedicated_threads: Option<u32>,
524 /// Syncer mode (e.g. `"distributed"`, `"centralized"`).
525 pub syncer_mode: Option<String>,
526 /// Log level used by the syncer.
527 pub syncer_log_level: Option<String>,
528 /// Whether the syncer supports on-the-fly reconfiguration.
529 pub support_syncer_reconf: Option<bool>,
530
531 // Gradual sync settings
532 /// Gradual-source sync mode.
533 pub gradual_src_mode: Option<String>,
534 /// Maximum number of sources synced concurrently in gradual mode.
535 pub gradual_src_max_sources: Option<u32>,
536 /// Gradual sync mode.
537 pub gradual_sync_mode: Option<String>,
538 /// Maximum number of shards synced concurrently per source in gradual mode.
539 pub gradual_sync_max_shards_per_source: Option<u32>,
540
541 // Slave and buffer settings
542 /// Replica output buffer limits.
543 pub slave_buffer: Option<String>,
544
545 // Snapshot settings
546 /// Snapshot policy entries (RDB save points).
547 pub snapshot_policy: Option<Vec<Value>>,
548
549 // Scheduling and recovery
550 /// Proxy scheduling policy (e.g. `"cmp"`, `"mnp"`, `"mru"`).
551 pub sched_policy: Option<String>,
552 /// Seconds to wait before automatic recovery (negative disables).
553 pub recovery_wait_time: Option<i32>,
554
555 // Performance and optimization
556 /// MULTI command optimization mode.
557 pub multi_commands_opt: Option<String>,
558 /// Configured ingress throughput cap.
559 pub throughput_ingress: Option<f64>,
560 /// Maximum number of keys tracked by the client-side caching tracking table.
561 pub tracking_table_max_keys: Option<u32>,
562 /// Whether the WAIT command is allowed.
563 pub wait_command: Option<bool>,
564
565 // Legacy and deprecated fields
566 /// Background operations currently running on the database (read-only).
567 pub background_op: Option<Vec<Value>>,
568
569 // Advanced configuration
570 /// Whether MKMS (multi-key multi-slot) commands are allowed.
571 pub mkms: Option<bool>,
572 /// Role-based permissions for the database.
573 pub roles_permissions: Option<Vec<Value>>,
574 /// Free-form tags attached to the database.
575 pub tags: Option<Vec<String>>,
576 /// Current topology epoch counter (read-only).
577 pub topology_epoch: Option<u32>,
578
579 /// Additive or version-specific response fields not yet modeled explicitly.
580 ///
581 /// Redis Software adds advanced database controls between supported release
582 /// families. Retaining them prevents typed reads from silently discarding
583 /// fields while stable public fields are promoted deliberately.
584 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
585 pub additional_fields: BTreeMap<String, Value>,
586}
587
588/// Database endpoint information
589#[derive(Debug, Clone, Serialize, Deserialize)]
590#[non_exhaustive]
591pub struct EndpointInfo {
592 /// Unique identifier for the endpoint
593 pub uid: Option<String>,
594 /// List of IP addresses for the endpoint
595 pub addr: Option<Vec<String>>,
596 /// Port number for the endpoint
597 pub port: Option<u16>,
598 /// DNS name for the endpoint
599 pub dns_name: Option<String>,
600 /// Proxy policy for the endpoint
601 pub proxy_policy: Option<String>,
602 /// Address type (e.g., "internal", "external")
603 pub addr_type: Option<String>,
604 /// OSS cluster API preferred IP type
605 pub oss_cluster_api_preferred_ip_type: Option<String>,
606 /// OSS cluster API preferred endpoint type (`ip` or `hostname`).
607 pub oss_cluster_api_preferred_endpoint_type: Option<String>,
608 /// List of proxy UIDs to exclude
609 pub exclude_proxies: Option<Vec<u32>>,
610 /// List of proxy UIDs to include
611 pub include_proxies: Option<Vec<u32>>,
612 /// Additive or version-specific endpoint fields.
613 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
614 pub additional_fields: BTreeMap<String, Value>,
615}
616
617/// Module configuration for database creation
618#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)]
619pub struct ModuleConfig {
620 /// Module name to load (e.g. `"search"`, `"timeseries"`, `"bf"`).
621 #[builder(setter(into))]
622 pub module_name: String,
623 /// Arguments passed to the module when it loads.
624 #[serde(skip_serializing_if = "Option::is_none")]
625 #[builder(default, setter(into, strip_option))]
626 pub module_args: Option<String>,
627}
628
629/// Create database request
630///
631/// # Examples
632///
633/// ```rust,no_run
634/// use redis_enterprise::{CreateDatabaseRequest, ModuleConfig};
635///
636/// let request = CreateDatabaseRequest::builder()
637/// .name("my-database")
638/// .memory_size(1024 * 1024 * 1024) // 1GB
639/// .redis_version("7.4") // Optional; choose a version advertised by /v1/nodes
640/// .port(12000)
641/// .replication(true)
642/// .persistence("aof")
643/// .eviction_policy("volatile-lru")
644/// .shards_count(2)
645/// .authentication_redis_pass("secure-password")
646/// .build();
647/// ```
648#[derive(Debug, Serialize, Deserialize, TypedBuilder)]
649pub struct CreateDatabaseRequest {
650 /// Database name.
651 #[builder(setter(into))]
652 pub name: String,
653 /// Database memory limit in bytes (0 for unlimited).
654 #[serde(skip_serializing_if = "Option::is_none")]
655 #[builder(default, setter(strip_option))]
656 pub memory_size: Option<u64>,
657 /// Redis database engine version to provision (for example, `"7.4"`).
658 ///
659 /// Supported values are advertised by each node's
660 /// `supported_database_versions` field. When omitted, Redis Software
661 /// chooses its configured default.
662 #[serde(skip_serializing_if = "Option::is_none")]
663 #[builder(default, setter(into, strip_option))]
664 pub redis_version: Option<String>,
665 /// TCP port on which the database is available (read-only).
666 #[serde(skip_serializing_if = "Option::is_none")]
667 #[builder(default, setter(strip_option))]
668 pub port: Option<u16>,
669 /// Whether in-memory replication is enabled.
670 #[serde(skip_serializing_if = "Option::is_none")]
671 #[builder(default, setter(strip_option))]
672 pub replication: Option<bool>,
673 /// Persistence policy (e.g. `"disabled"`, `"aof"`, `"snapshot"`).
674 ///
675 /// Redis Software names this field `data_persistence` on the wire. The
676 /// shorter Rust field and builder name are retained for source
677 /// compatibility.
678 #[serde(
679 rename = "data_persistence",
680 alias = "persistence",
681 skip_serializing_if = "Option::is_none"
682 )]
683 #[builder(default, setter(into, strip_option))]
684 pub persistence: Option<String>,
685 /// Eviction policy when the database reaches its memory limit (e.g. `"allkeys-lru"`).
686 #[serde(skip_serializing_if = "Option::is_none")]
687 #[builder(default, setter(into, strip_option))]
688 pub eviction_policy: Option<String>,
689 /// Whether the database is sharded.
690 #[serde(skip_serializing_if = "Option::is_none")]
691 #[builder(default, setter(strip_option))]
692 pub sharding: Option<bool>,
693 /// Number of database shards.
694 #[serde(skip_serializing_if = "Option::is_none")]
695 #[builder(default, setter(strip_option))]
696 pub shards_count: Option<u32>,
697 /// Shard count.
698 #[serde(skip_serializing_if = "Option::is_none", alias = "shard_count")]
699 #[builder(default, setter(strip_option))]
700 pub shard_count: Option<u32>,
701 /// Proxy policy (e.g. `"single"`, `"all-master-shards"`, `"all-nodes"`).
702 #[serde(skip_serializing_if = "Option::is_none")]
703 #[builder(default, setter(into, strip_option))]
704 pub proxy_policy: Option<String>,
705 /// Whether rack-aware shard placement is enabled.
706 #[serde(skip_serializing_if = "Option::is_none")]
707 #[builder(default, setter(strip_option))]
708 pub rack_aware: Option<bool>,
709 /// List of Redis modules loaded into the database.
710 #[serde(skip_serializing_if = "Option::is_none")]
711 #[builder(default, setter(strip_option))]
712 pub module_list: Option<Vec<ModuleConfig>>,
713 /// Whether this database participates in a CRDT (Active-Active) deployment.
714 #[serde(skip_serializing_if = "Option::is_none")]
715 #[builder(default, setter(strip_option))]
716 pub crdt: Option<bool>,
717 /// Redis password used for client authentication.
718 #[serde(skip_serializing_if = "Option::is_none")]
719 #[builder(default, setter(into, strip_option))]
720 pub authentication_redis_pass: Option<String>,
721}
722
723/// Database handler for executing database commands
724pub struct DatabaseHandler {
725 client: RestClient,
726}
727
728impl DatabaseHandler {
729 /// New.
730 pub fn new(client: RestClient) -> Self {
731 DatabaseHandler { client }
732 }
733
734 /// List all databases (BDB.LIST)
735 pub async fn list(&self) -> Result<Vec<DatabaseInfo>> {
736 self.client.get("/v1/bdbs").await
737 }
738
739 /// Get specific database info (BDB.INFO)
740 pub async fn info(&self, uid: u32) -> Result<DatabaseInfo> {
741 self.client.get(&format!("/v1/bdbs/{}", uid)).await
742 }
743
744 /// Get specific database info (alias for info)
745 pub async fn get(&self, uid: u32) -> Result<DatabaseInfo> {
746 self.info(uid).await
747 }
748
749 /// Create a new database (BDB.CREATE)
750 pub async fn create(&self, request: CreateDatabaseRequest) -> Result<DatabaseInfo> {
751 self.client.post("/v1/bdbs", &request).await
752 }
753
754 /// Update database configuration (BDB.UPDATE)
755 pub async fn update(&self, uid: u32, updates: Value) -> Result<DatabaseInfo> {
756 self.client
757 .put(&format!("/v1/bdbs/{}", uid), &updates)
758 .await
759 }
760
761 /// Delete a database (BDB.DELETE)
762 pub async fn delete(&self, uid: u32) -> Result<()> {
763 self.client.delete(&format!("/v1/bdbs/{}", uid)).await
764 }
765
766 /// Get database stats (BDB.STATS)
767 pub async fn stats(&self, uid: u32) -> Result<Value> {
768 self.client.get(&format!("/v1/bdbs/stats/{}", uid)).await
769 }
770
771 /// Get database metrics through the canonical database statistics route.
772 ///
773 /// Redis Software does not register `/v1/bdbs/metrics/{uid}`. Keep this
774 /// convenience alias for callers that use metrics terminology.
775 pub async fn metrics(&self, uid: u32) -> Result<Value> {
776 self.stats(uid).await
777 }
778
779 /// Export database (BDB.EXPORT)
780 pub async fn export(&self, uid: u32, export_location: &str) -> Result<ExportResponse> {
781 let body = serde_json::json!({
782 "export_location": export_location
783 });
784 self.client
785 .post(&format!("/v1/bdbs/{}/actions/export", uid), &body)
786 .await
787 }
788
789 /// Import database (BDB.IMPORT)
790 pub async fn import(
791 &self,
792 uid: u32,
793 import_location: &str,
794 flush: bool,
795 ) -> Result<ImportResponse> {
796 let body = serde_json::json!({
797 "import_location": import_location,
798 "flush": flush
799 });
800 self.client
801 .post(&format!("/v1/bdbs/{}/actions/import", uid), &body)
802 .await
803 }
804
805 /// Flush all keys from a database.
806 ///
807 /// `PUT /v1/bdbs/{uid}/flush`. This is the path-segment-style action
808 /// the REST API documents; the previous implementation POSTed to
809 /// `/v1/bdbs/{uid}/actions/flush`, which is not in the spec.
810 pub async fn flush(&self, uid: u32) -> Result<DatabaseActionResponse> {
811 let response = self
812 .client
813 .put_raw(&format!("/v1/bdbs/{}/flush", uid), serde_json::json!({}))
814 .await?;
815 serde_json::from_value(response).map_err(Into::into)
816 }
817
818 /// Get database shards (BDB.SHARDS)
819 pub async fn shards(&self, uid: u32) -> Result<Value> {
820 self.client.get(&format!("/v1/bdbs/{}/shards", uid)).await
821 }
822
823 /// Retired database endpoints helper.
824 #[deprecated(note = "Redis Software does not register database-scoped endpoint routes")]
825 pub async fn endpoints(&self, _uid: u32) -> Result<Vec<EndpointInfo>> {
826 crate::error::unsupported_operation("list database endpoints")
827 }
828
829 /// Optimize shards placement (status) - GET
830 pub async fn optimize_shards_placement(&self, uid: u32) -> Result<Value> {
831 self.client
832 .get(&format!(
833 "/v1/bdbs/{}/actions/optimize_shards_placement",
834 uid
835 ))
836 .await
837 }
838
839 /// Recover database (status) - GET
840 pub async fn recover_status(&self, uid: u32) -> Result<Value> {
841 self.client
842 .get(&format!("/v1/bdbs/{}/actions/recover", uid))
843 .await
844 }
845
846 /// Recover database - POST
847 pub async fn recover(&self, uid: u32) -> Result<DatabaseActionResponse> {
848 self.client
849 .post(
850 &format!("/v1/bdbs/{}/actions/recover", uid),
851 &serde_json::json!({}),
852 )
853 .await
854 }
855
856 /// Resume traffic - POST
857 pub async fn resume_traffic(&self, uid: u32) -> Result<DatabaseActionResponse> {
858 self.client
859 .post(
860 &format!("/v1/bdbs/{}/actions/resume_traffic", uid),
861 &serde_json::json!({}),
862 )
863 .await
864 }
865
866 /// Stop traffic - POST
867 pub async fn stop_traffic(&self, uid: u32) -> Result<DatabaseActionResponse> {
868 self.client
869 .post(
870 &format!("/v1/bdbs/{}/actions/stop_traffic", uid),
871 &serde_json::json!({}),
872 )
873 .await
874 }
875
876 /// Rebalance database - PUT
877 pub async fn rebalance(&self, uid: u32) -> Result<DatabaseActionResponse> {
878 self.client
879 .put(
880 &format!("/v1/bdbs/{}/actions/rebalance", uid),
881 &serde_json::json!({}),
882 )
883 .await
884 }
885
886 /// Revamp database - PUT
887 pub async fn revamp(&self, uid: u32) -> Result<DatabaseActionResponse> {
888 self.client
889 .put(
890 &format!("/v1/bdbs/{}/actions/revamp", uid),
891 &serde_json::json!({}),
892 )
893 .await
894 }
895
896 /// Reset backup status - PUT
897 pub async fn backup_reset_status(&self, uid: u32) -> Result<Value> {
898 self.client
899 .put(
900 &format!("/v1/bdbs/{}/actions/backup_reset_status", uid),
901 &serde_json::json!({}),
902 )
903 .await
904 }
905
906 /// Reset export status - PUT
907 pub async fn export_reset_status(&self, uid: u32) -> Result<Value> {
908 self.client
909 .put(
910 &format!("/v1/bdbs/{}/actions/export_reset_status", uid),
911 &serde_json::json!({}),
912 )
913 .await
914 }
915
916 /// Reset import status - PUT
917 pub async fn import_reset_status(&self, uid: u32) -> Result<Value> {
918 self.client
919 .put(
920 &format!("/v1/bdbs/{}/actions/import_reset_status", uid),
921 &serde_json::json!({}),
922 )
923 .await
924 }
925
926 /// Peer stats for a database - GET
927 pub async fn peer_stats(&self, uid: u32) -> Result<Value> {
928 self.client
929 .get(&format!("/v1/bdbs/{}/peer_stats", uid))
930 .await
931 }
932
933 /// Peer stats for a specific peer - GET
934 pub async fn peer_stats_for(&self, uid: u32, peer_uid: u32) -> Result<Value> {
935 self.client
936 .get(&format!("/v1/bdbs/{}/peer_stats/{}", uid, peer_uid))
937 .await
938 }
939
940 /// Sync source stats for a database - GET
941 pub async fn sync_source_stats(&self, uid: u32) -> Result<Value> {
942 self.client
943 .get(&format!("/v1/bdbs/{}/sync_source_stats", uid))
944 .await
945 }
946
947 /// Sync source stats for a specific source - GET
948 pub async fn sync_source_stats_for(&self, uid: u32, src_uid: u32) -> Result<Value> {
949 self.client
950 .get(&format!("/v1/bdbs/{}/sync_source_stats/{}", uid, src_uid))
951 .await
952 }
953
954 /// Syncer state (all) - GET
955 pub async fn syncer_state(&self, uid: u32) -> Result<Value> {
956 self.client
957 .get(&format!("/v1/bdbs/{}/syncer_state", uid))
958 .await
959 }
960
961 /// Syncer state for CRDT - GET
962 pub async fn syncer_state_crdt(&self, uid: u32) -> Result<Value> {
963 self.client
964 .get(&format!("/v1/bdbs/{}/syncer_state/crdt", uid))
965 .await
966 }
967
968 /// Syncer state for replica - GET
969 pub async fn syncer_state_replica(&self, uid: u32) -> Result<Value> {
970 self.client
971 .get(&format!("/v1/bdbs/{}/syncer_state/replica", uid))
972 .await
973 }
974
975 /// Database passwords delete - DELETE
976 pub async fn passwords_delete(&self, uid: u32) -> Result<()> {
977 self.client
978 .delete(&format!("/v1/bdbs/{}/passwords", uid))
979 .await
980 }
981
982 /// List all database alerts - GET
983 pub async fn alerts_all(&self) -> Result<Value> {
984 self.client.get("/v1/bdbs/alerts").await
985 }
986
987 /// List alerts for a specific database - GET
988 pub async fn alerts_for(&self, uid: u32) -> Result<Value> {
989 self.client.get(&format!("/v1/bdbs/alerts/{}", uid)).await
990 }
991
992 /// Get a specific alert for a database - GET
993 pub async fn alert_detail(&self, uid: u32, alert: &str) -> Result<Value> {
994 self.client
995 .get(&format!("/v1/bdbs/alerts/{}/{}", uid, alert))
996 .await
997 }
998
999 /// CRDT source alerts - GET
1000 pub async fn crdt_source_alerts_all(&self) -> Result<Value> {
1001 self.client.get("/v1/bdbs/crdt_sources/alerts").await
1002 }
1003
1004 /// CRDT source alerts for DB - GET
1005 pub async fn crdt_source_alerts_for(&self, uid: u32) -> Result<Value> {
1006 self.client
1007 .get(&format!("/v1/bdbs/crdt_sources/alerts/{}", uid))
1008 .await
1009 }
1010
1011 /// CRDT source alerts for specific source - GET
1012 pub async fn crdt_source_alerts_source(&self, uid: u32, source_id: u32) -> Result<Value> {
1013 self.client
1014 .get(&format!(
1015 "/v1/bdbs/crdt_sources/alerts/{}/{}",
1016 uid, source_id
1017 ))
1018 .await
1019 }
1020
1021 /// CRDT source alert detail - GET
1022 pub async fn crdt_source_alert_detail(
1023 &self,
1024 uid: u32,
1025 source_id: u32,
1026 alert: &str,
1027 ) -> Result<Value> {
1028 self.client
1029 .get(&format!(
1030 "/v1/bdbs/crdt_sources/alerts/{}/{}/{}",
1031 uid, source_id, alert
1032 ))
1033 .await
1034 }
1035
1036 /// Replica source alerts - GET
1037 pub async fn replica_source_alerts_all(&self) -> Result<Value> {
1038 self.client.get("/v1/bdbs/replica_sources/alerts").await
1039 }
1040
1041 /// Replica source alerts for DB - GET
1042 pub async fn replica_source_alerts_for(&self, uid: u32) -> Result<Value> {
1043 self.client
1044 .get(&format!("/v1/bdbs/replica_sources/alerts/{}", uid))
1045 .await
1046 }
1047
1048 /// Replica source alerts for specific source - GET
1049 pub async fn replica_source_alerts_source(&self, uid: u32, source_id: u32) -> Result<Value> {
1050 self.client
1051 .get(&format!(
1052 "/v1/bdbs/replica_sources/alerts/{}/{}",
1053 uid, source_id
1054 ))
1055 .await
1056 }
1057
1058 /// Replica source alert detail - GET
1059 pub async fn replica_source_alert_detail(
1060 &self,
1061 uid: u32,
1062 source_id: u32,
1063 alert: &str,
1064 ) -> Result<Value> {
1065 self.client
1066 .get(&format!(
1067 "/v1/bdbs/replica_sources/alerts/{}/{}/{}",
1068 uid, source_id, alert
1069 ))
1070 .await
1071 }
1072
1073 /// Upgrade database Redis version and/or modules (BDB.UPGRADE)
1074 ///
1075 /// # Examples
1076 ///
1077 /// ```no_run
1078 /// # use redis_enterprise::EnterpriseClient;
1079 /// # use redis_enterprise::bdb::DatabaseUpgradeRequest;
1080 /// # async fn example() -> redis_enterprise::Result<()> {
1081 /// let client = EnterpriseClient::builder()
1082 /// .base_url("https://localhost:9443")
1083 /// .username("admin")
1084 /// .password("password")
1085 /// .insecure(true)
1086 /// .build()?;
1087 ///
1088 /// // Upgrade to latest Redis version
1089 /// let request = DatabaseUpgradeRequest {
1090 /// redis_version: None, // defaults to latest
1091 /// preserve_roles: Some(true),
1092 /// ..Default::default()
1093 /// };
1094 /// client.databases().upgrade_redis_version(1, request).await?;
1095 ///
1096 /// // Upgrade to specific Redis version
1097 /// let request = DatabaseUpgradeRequest {
1098 /// redis_version: Some("7.4.2".to_string()),
1099 /// preserve_roles: Some(true),
1100 /// ..Default::default()
1101 /// };
1102 /// client.databases().upgrade_redis_version(1, request).await?;
1103 /// # Ok(())
1104 /// # }
1105 /// ```
1106 pub async fn upgrade_redis_version(
1107 &self,
1108 uid: u32,
1109 request: DatabaseUpgradeRequest,
1110 ) -> Result<DatabaseActionResponse> {
1111 self.client
1112 .post(&format!("/v1/bdbs/{}/upgrade", uid), &request)
1113 .await
1114 }
1115
1116 /// Reset the database admin password.
1117 ///
1118 /// `PUT /v1/bdbs/{uid}/reset_admin_pass`. This is the path-segment-style
1119 /// action the REST API documents. The new password is sent in the body
1120 /// under the `authentication_redis_pass` key.
1121 pub async fn reset_admin_pass(
1122 &self,
1123 uid: u32,
1124 new_password: &str,
1125 ) -> Result<DatabaseActionResponse> {
1126 let body = serde_json::json!({
1127 "authentication_redis_pass": new_password
1128 });
1129 let response = self
1130 .client
1131 .put_raw(&format!("/v1/bdbs/{}/reset_admin_pass", uid), body)
1132 .await?;
1133 serde_json::from_value(response).map_err(Into::into)
1134 }
1135
1136 /// Reset database password.
1137 ///
1138 /// Deprecated alias for [`Self::reset_admin_pass`]. The previous
1139 /// implementation POSTed to `/v1/bdbs/{uid}/actions/reset_password`,
1140 /// which is not in the REST API spec.
1141 #[deprecated(
1142 since = "0.9.0",
1143 note = "use `reset_admin_pass`; the action POST path was not in the REST API spec"
1144 )]
1145 pub async fn reset_password(
1146 &self,
1147 uid: u32,
1148 new_password: &str,
1149 ) -> Result<DatabaseActionResponse> {
1150 self.reset_admin_pass(uid, new_password).await
1151 }
1152
1153 /// Check database availability.
1154 ///
1155 /// Redis Software returns an empty successful response for this endpoint,
1156 /// so availability is represented by `Ok(())` rather than a JSON value.
1157 pub async fn availability(&self, uid: u32) -> Result<()> {
1158 self.client
1159 .get_empty(&format!("/v1/bdbs/{}/availability", uid))
1160 .await
1161 }
1162
1163 /// Check local database endpoint availability.
1164 ///
1165 /// Redis Software returns an empty successful response for this endpoint,
1166 /// so availability is represented by `Ok(())` rather than a JSON value.
1167 pub async fn endpoint_availability(&self, uid: u32) -> Result<()> {
1168 self.client
1169 .get_empty(&format!("/v1/local/bdbs/{}/endpoint/availability", uid))
1170 .await
1171 }
1172
1173 /// Create database using v2 API (supports recovery plan)
1174 pub async fn create_v2(&self, request: Value) -> Result<DatabaseInfo> {
1175 self.client.post("/v2/bdbs", &request).await
1176 }
1177
1178 /// Watch database status changes in real-time
1179 ///
1180 /// Polls the database endpoint and yields updates when status changes occur.
1181 /// Useful for monitoring database operations like upgrades, migrations, backups, etc.
1182 ///
1183 /// # Arguments
1184 /// * `uid` - Database ID to watch
1185 /// * `poll_interval` - Time to wait between polls
1186 ///
1187 /// # Returns
1188 /// A stream of `(DatabaseInfo, Option<String>)` tuples where:
1189 /// - `DatabaseInfo` - Current database state
1190 /// - `Option<String>` - Previous status (None on first poll, Some on status change)
1191 ///
1192 /// # Example
1193 /// ```no_run
1194 /// use redis_enterprise::EnterpriseClient;
1195 /// use futures::StreamExt;
1196 /// use std::time::Duration;
1197 ///
1198 /// # async fn example(client: EnterpriseClient) -> Result<(), Box<dyn std::error::Error>> {
1199 /// let db_handler = client.databases();
1200 /// let mut stream = db_handler.watch_database(1, Duration::from_secs(5));
1201 ///
1202 /// while let Some(result) = stream.next().await {
1203 /// match result {
1204 /// Ok((db_info, prev_status)) => {
1205 /// if let Some(old_status) = prev_status {
1206 /// println!("Status changed: {} -> {}", old_status, db_info.status.unwrap_or_default());
1207 /// } else {
1208 /// println!("Initial status: {}", db_info.status.unwrap_or_default());
1209 /// }
1210 /// }
1211 /// Err(e) => eprintln!("Error: {}", e),
1212 /// }
1213 /// }
1214 /// # Ok(())
1215 /// # }
1216 /// ```
1217 pub fn watch_database(&self, uid: u32, poll_interval: Duration) -> DatabaseWatchStream<'_> {
1218 Box::pin(async_stream::stream! {
1219 let mut last_status: Option<String> = None;
1220
1221 loop {
1222 match self.info(uid).await {
1223 Ok(db_info) => {
1224 let current_status = db_info.status.clone();
1225
1226 // Check if status changed
1227 let status_changed = match (&last_status, ¤t_status) {
1228 (Some(old), Some(new)) => old != new,
1229 (None, Some(_)) => false, // First poll, not a change
1230 (Some(_), None) => true, // Status disappeared
1231 (None, None) => false,
1232 };
1233
1234 // Yield the database info with previous status if changed
1235 if status_changed {
1236 yield Ok((db_info, last_status.clone()));
1237 } else if last_status.is_none() {
1238 // First poll - always yield
1239 yield Ok((db_info, None));
1240 } else {
1241 // Status unchanged - yield current state for monitoring
1242 yield Ok((db_info, None));
1243 }
1244
1245 last_status = current_status;
1246 }
1247 Err(e) => {
1248 yield Err(e);
1249 break;
1250 }
1251 }
1252
1253 sleep(poll_interval).await;
1254 }
1255 })
1256 }
1257}