metabase_api_rs/core/models/
database.rs

1//! Database model representing Metabase database connections
2//!
3//! This module provides the core data structures for working with
4//! Metabase database connections, including tables and fields.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10use super::common::MetabaseId;
11
12/// Connection source for database
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
14#[serde(rename_all = "lowercase")]
15pub enum ConnectionSource {
16    #[default]
17    Admin,
18    Setup,
19}
20
21/// Unique identifier for a database field
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub struct FieldId(pub i64);
24
25/// Unique identifier for a database table
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub struct TableId(pub i64);
28
29/// Represents a Metabase database connection
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub struct Database {
32    /// Unique identifier for the database
33    pub id: MetabaseId,
34
35    /// Database name
36    pub name: String,
37
38    /// Database engine (e.g., "postgres", "mysql", "h2")
39    pub engine: String,
40
41    /// Connection details (host, port, database name, etc.)
42    pub details: Value,
43
44    /// Whether full sync is enabled
45    #[serde(default)]
46    pub is_full_sync: bool,
47
48    /// Whether on-demand sync is enabled
49    #[serde(default)]
50    pub is_on_demand: bool,
51
52    /// Whether the database is a sample database
53    #[serde(default)]
54    pub is_sample: bool,
55
56    /// Cache field values for this database
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub cache_field_values_schedule: Option<String>,
59
60    /// Metadata sync schedule
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub metadata_sync_schedule: Option<String>,
63
64    /// When the database was created
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub created_at: Option<DateTime<Utc>>,
67
68    /// When the database was last updated
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub updated_at: Option<DateTime<Utc>>,
71}
72
73/// Represents a table in a database
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub struct DatabaseTable {
76    /// Unique identifier for the table
77    pub id: TableId,
78
79    /// Database ID this table belongs to
80    pub db_id: MetabaseId,
81
82    /// Table name
83    pub name: String,
84
85    /// Database schema name
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub schema: Option<String>,
88
89    /// Display name for the table
90    pub display_name: String,
91
92    /// Whether the table is active
93    #[serde(default = "default_true")]
94    pub active: bool,
95
96    /// Table description
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub description: Option<String>,
99
100    /// Entity type (e.g., "entity/GenericTable", "entity/EventTable")
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub entity_type: Option<String>,
103
104    /// Visibility type
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub visibility_type: Option<String>,
107
108    /// When the table was created
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub created_at: Option<DateTime<Utc>>,
111
112    /// When the table was last updated
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub updated_at: Option<DateTime<Utc>>,
115}
116
117/// Represents a field in a database table
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub struct DatabaseField {
120    /// Unique identifier for the field
121    pub id: FieldId,
122
123    /// Table ID this field belongs to
124    pub table_id: TableId,
125
126    /// Field name
127    pub name: String,
128
129    /// Display name for the field
130    pub display_name: String,
131
132    /// Database-specific type
133    pub database_type: String,
134
135    /// Base type (e.g., "type/Text", "type/Integer")
136    pub base_type: String,
137
138    /// Semantic type (e.g., "type/Email", "type/URL")
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub semantic_type: Option<String>,
141
142    /// Whether this field is active
143    #[serde(default = "default_true")]
144    pub active: bool,
145
146    /// Field description
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub description: Option<String>,
149
150    /// Whether this is a primary key
151    #[serde(default)]
152    pub is_pk: bool,
153
154    /// Foreign key target field ID
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub fk_target_field_id: Option<FieldId>,
157
158    /// Field position in the table
159    #[serde(default)]
160    pub position: i32,
161
162    /// Visibility type
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub visibility_type: Option<String>,
165
166    /// When the field was created
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub created_at: Option<DateTime<Utc>>,
169
170    /// When the field was last updated
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub updated_at: Option<DateTime<Utc>>,
173}
174
175/// Request to create a new database connection
176#[derive(Debug, Clone, Serialize)]
177pub struct CreateDatabaseRequest {
178    /// Database name
179    pub name: String,
180
181    /// Database engine
182    pub engine: String,
183
184    /// Connection details
185    pub details: Value,
186
187    /// Whether to enable full sync
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub is_full_sync: Option<bool>,
190
191    /// Whether to enable on-demand sync
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub is_on_demand: Option<bool>,
194
195    /// Schedule for caching field values
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub cache_field_values_schedule: Option<String>,
198
199    /// Schedule for metadata sync
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub metadata_sync_schedule: Option<String>,
202}
203
204/// Request to update a database connection
205#[derive(Debug, Clone, Default, Serialize)]
206pub struct UpdateDatabaseRequest {
207    /// New name
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub name: Option<String>,
210
211    /// New connection details
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub details: Option<Value>,
214
215    /// Whether to enable full sync
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub is_full_sync: Option<bool>,
218
219    /// Whether to enable on-demand sync
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub is_on_demand: Option<bool>,
222
223    /// Schedule for caching field values
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub cache_field_values_schedule: Option<String>,
226
227    /// Schedule for metadata sync
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub metadata_sync_schedule: Option<String>,
230}
231
232/// Database sync status
233#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
234pub struct DatabaseSyncStatus {
235    /// Current sync status
236    pub status: String,
237
238    /// Sync progress (0-100)
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub progress: Option<f32>,
241
242    /// Error message if sync failed
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub error: Option<String>,
245
246    /// When the sync started
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub started_at: Option<DateTime<Utc>>,
249
250    /// When the sync completed
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub completed_at: Option<DateTime<Utc>>,
253}
254
255fn default_true() -> bool {
256    true
257}
258
259impl Database {
260    /// Creates a new database builder
261    pub fn builder(name: impl Into<String>, engine: impl Into<String>) -> DatabaseBuilder {
262        DatabaseBuilder::new(name, engine)
263    }
264}
265
266/// Builder for creating Database instances
267pub struct DatabaseBuilder {
268    name: String,
269    engine: String,
270    details: Value,
271    is_full_sync: bool,
272    is_on_demand: bool,
273    cache_field_values_schedule: Option<String>,
274    metadata_sync_schedule: Option<String>,
275}
276
277impl DatabaseBuilder {
278    /// Creates a new database builder
279    pub fn new(name: impl Into<String>, engine: impl Into<String>) -> Self {
280        Self {
281            name: name.into(),
282            engine: engine.into(),
283            details: Value::Object(serde_json::Map::new()),
284            is_full_sync: true,
285            is_on_demand: false,
286            cache_field_values_schedule: None,
287            metadata_sync_schedule: None,
288        }
289    }
290
291    /// Sets the connection details
292    pub fn details(mut self, details: Value) -> Self {
293        self.details = details;
294        self
295    }
296
297    /// Sets whether full sync is enabled
298    pub fn full_sync(mut self, enabled: bool) -> Self {
299        self.is_full_sync = enabled;
300        self
301    }
302
303    /// Sets whether on-demand sync is enabled
304    pub fn on_demand_sync(mut self, enabled: bool) -> Self {
305        self.is_on_demand = enabled;
306        self
307    }
308
309    /// Sets the cache field values schedule
310    pub fn cache_schedule(mut self, schedule: impl Into<String>) -> Self {
311        self.cache_field_values_schedule = Some(schedule.into());
312        self
313    }
314
315    /// Sets the metadata sync schedule
316    pub fn sync_schedule(mut self, schedule: impl Into<String>) -> Self {
317        self.metadata_sync_schedule = Some(schedule.into());
318        self
319    }
320
321    /// Builds the Database instance
322    pub fn build(self) -> Database {
323        Database {
324            id: MetabaseId(0), // Will be set by the server
325            name: self.name,
326            engine: self.engine,
327            details: self.details,
328            is_full_sync: self.is_full_sync,
329            is_on_demand: self.is_on_demand,
330            is_sample: false,
331            cache_field_values_schedule: self.cache_field_values_schedule,
332            metadata_sync_schedule: self.metadata_sync_schedule,
333            created_at: None,
334            updated_at: None,
335        }
336    }
337
338    /// Builds a CreateDatabaseRequest
339    pub fn build_request(self) -> CreateDatabaseRequest {
340        CreateDatabaseRequest {
341            name: self.name,
342            engine: self.engine,
343            details: self.details,
344            is_full_sync: Some(self.is_full_sync),
345            is_on_demand: Some(self.is_on_demand),
346            cache_field_values_schedule: self.cache_field_values_schedule,
347            metadata_sync_schedule: self.metadata_sync_schedule,
348        }
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355    use serde_json::json;
356
357    #[test]
358    fn test_database_creation() {
359        let database = Database::builder("Test DB", "postgres")
360            .details(json!({
361                "host": "localhost",
362                "port": 5432,
363                "dbname": "testdb",
364                "user": "testuser"
365            }))
366            .full_sync(true)
367            .on_demand_sync(false)
368            .build();
369
370        assert_eq!(database.name, "Test DB");
371        assert_eq!(database.engine, "postgres");
372        assert!(database.is_full_sync);
373        assert!(!database.is_on_demand);
374    }
375
376    #[test]
377    fn test_database_table() {
378        let table = DatabaseTable {
379            id: TableId(1),
380            db_id: MetabaseId(1),
381            name: "users".to_string(),
382            schema: Some("public".to_string()),
383            display_name: "Users".to_string(),
384            active: true,
385            description: Some("User accounts".to_string()),
386            entity_type: Some("entity/UserTable".to_string()),
387            visibility_type: None,
388            created_at: None,
389            updated_at: None,
390        };
391
392        assert_eq!(table.name, "users");
393        assert_eq!(table.display_name, "Users");
394        assert!(table.active);
395    }
396
397    #[test]
398    fn test_database_field() {
399        let field = DatabaseField {
400            id: FieldId(1),
401            table_id: TableId(1),
402            name: "email".to_string(),
403            display_name: "Email".to_string(),
404            database_type: "VARCHAR(255)".to_string(),
405            base_type: "type/Text".to_string(),
406            semantic_type: Some("type/Email".to_string()),
407            active: true,
408            description: None,
409            is_pk: false,
410            fk_target_field_id: None,
411            position: 2,
412            visibility_type: None,
413            created_at: None,
414            updated_at: None,
415        };
416
417        assert_eq!(field.name, "email");
418        assert_eq!(field.base_type, "type/Text");
419        assert_eq!(field.semantic_type, Some("type/Email".to_string()));
420        assert!(!field.is_pk);
421    }
422
423    #[test]
424    fn test_create_database_request() {
425        let request = Database::builder("Production DB", "mysql")
426            .details(json!({
427                "host": "db.example.com",
428                "port": 3306,
429                "dbname": "production"
430            }))
431            .cache_schedule("0 0 * * *")
432            .build_request();
433
434        assert_eq!(request.name, "Production DB");
435        assert_eq!(request.engine, "mysql");
436        assert_eq!(request.is_full_sync, Some(true));
437        assert_eq!(
438            request.cache_field_values_schedule,
439            Some("0 0 * * *".to_string())
440        );
441    }
442
443    #[test]
444    fn test_update_database_request() {
445        let request = UpdateDatabaseRequest {
446            name: Some("Updated DB".to_string()),
447            is_full_sync: Some(false),
448            ..Default::default()
449        };
450
451        assert_eq!(request.name, Some("Updated DB".to_string()));
452        assert_eq!(request.is_full_sync, Some(false));
453        assert!(request.details.is_none());
454    }
455}
456
457// ==================== Database Metadata Models ====================
458
459/// Database metadata including tables and fields
460#[derive(Debug, Clone, Serialize, Deserialize)]
461pub struct DatabaseMetadata {
462    /// Database ID
463    pub id: MetabaseId,
464
465    /// Database name
466    pub name: String,
467
468    /// Database engine
469    pub engine: String,
470
471    /// List of tables in the database
472    pub tables: Vec<TableMetadata>,
473
474    /// Database features
475    #[serde(default)]
476    pub features: Vec<String>,
477
478    /// Native query permissions
479    #[serde(skip_serializing_if = "Option::is_none")]
480    pub native_permissions: Option<String>,
481}
482
483/// Table metadata including fields
484#[derive(Debug, Clone, Serialize, Deserialize)]
485pub struct TableMetadata {
486    /// Table ID
487    pub id: TableId,
488
489    /// Table name
490    pub name: String,
491
492    /// Table schema
493    #[serde(skip_serializing_if = "Option::is_none")]
494    pub schema: Option<String>,
495
496    /// Display name
497    pub display_name: String,
498
499    /// Table description
500    #[serde(skip_serializing_if = "Option::is_none")]
501    pub description: Option<String>,
502
503    /// Entity type
504    #[serde(skip_serializing_if = "Option::is_none")]
505    pub entity_type: Option<String>,
506
507    /// List of fields in the table
508    pub fields: Vec<FieldMetadata>,
509
510    /// Whether the table is active
511    #[serde(default = "default_true")]
512    pub active: bool,
513}
514
515/// Field metadata
516#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct FieldMetadata {
518    /// Field ID
519    pub id: FieldId,
520
521    /// Field name
522    pub name: String,
523
524    /// Display name
525    pub display_name: String,
526
527    /// Database type
528    pub database_type: String,
529
530    /// Base type (Metabase type)
531    pub base_type: String,
532
533    /// Semantic type
534    #[serde(skip_serializing_if = "Option::is_none")]
535    pub semantic_type: Option<String>,
536
537    /// Field description
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub description: Option<String>,
540
541    /// Whether this is a primary key
542    #[serde(default)]
543    pub is_pk: bool,
544
545    /// Foreign key target field ID
546    #[serde(skip_serializing_if = "Option::is_none")]
547    pub fk_target_field_id: Option<FieldId>,
548
549    /// Field position in the table
550    pub position: i32,
551
552    /// Whether the field is active
553    #[serde(default = "default_true")]
554    pub active: bool,
555}
556
557/// Database sync result
558#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct SyncResult {
560    /// Sync task ID
561    pub id: String,
562
563    /// Sync status
564    pub status: String,
565
566    /// Status message
567    #[serde(skip_serializing_if = "Option::is_none")]
568    pub message: Option<String>,
569
570    /// When the sync started
571    #[serde(skip_serializing_if = "Option::is_none")]
572    pub started_at: Option<DateTime<Utc>>,
573
574    /// When the sync completed
575    #[serde(skip_serializing_if = "Option::is_none")]
576    pub completed_at: Option<DateTime<Utc>>,
577}