Skip to main content

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, &current_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}