1use chrono::{DateTime, Utc};
16use mz_sql::plan::AutoScalingStrategy;
17use std::fmt;
18use std::str::FromStr;
19
20use crate::project::SchemaQualifier;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
24#[serde(rename_all = "snake_case")]
25pub enum DeploymentKind {
26 Tables,
28 Objects,
30 Sinks,
32 Replacement,
34}
35
36impl fmt::Display for DeploymentKind {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 DeploymentKind::Tables => write!(f, "tables"),
40 DeploymentKind::Objects => write!(f, "objects"),
41 DeploymentKind::Sinks => write!(f, "sinks"),
42 DeploymentKind::Replacement => write!(f, "replacement"),
43 }
44 }
45}
46
47impl FromStr for DeploymentKind {
48 type Err = String;
49
50 fn from_str(s: &str) -> Result<Self, Self::Err> {
51 match s {
52 "tables" => Ok(DeploymentKind::Tables),
53 "objects" => Ok(DeploymentKind::Objects),
54 "sinks" => Ok(DeploymentKind::Sinks),
55 "replacement" => Ok(DeploymentKind::Replacement),
56 _ => Err(format!("Invalid deployment kind: {}", s)),
57 }
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
66#[serde(rename_all = "snake_case")]
67pub enum DeploymentMode {
68 Stage,
70}
71
72impl fmt::Display for DeploymentMode {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 match self {
75 DeploymentMode::Stage => write!(f, "stage"),
76 }
77 }
78}
79
80impl FromStr for DeploymentMode {
81 type Err = String;
82
83 fn from_str(s: &str) -> Result<Self, Self::Err> {
84 match s {
85 "stage" => Ok(DeploymentMode::Stage),
86 _ => Err(format!("Invalid deployment mode: {}", s)),
87 }
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct Cluster {
96 pub id: String,
98 pub name: String,
100 pub size: Option<String>,
102 pub replication_factor: Option<i64>,
104 pub auto_scaling_strategy: Option<AutoScalingStrategy>,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct ClusterOptions {
112 pub size: String,
114 pub replication_factor: u32,
116 pub auto_scaling_strategy: Option<AutoScalingStrategy>,
118}
119
120impl ClusterOptions {
121 pub fn from_cluster(cluster: &Cluster) -> Result<Self, String> {
123 let size = cluster.size.clone().ok_or_else(|| {
124 format!(
125 "Cluster '{}' has no size (unmanaged cluster?)",
126 cluster.name
127 )
128 })?;
129
130 let replication_factor = cluster
131 .replication_factor
132 .unwrap_or(1)
133 .try_into()
134 .map_err(|_| format!("Invalid replication_factor for cluster '{}'", cluster.name))?;
135
136 Ok(Self {
137 size,
138 replication_factor,
139 auto_scaling_strategy: cluster.auto_scaling_strategy.clone(),
140 })
141 }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct ClusterReplica {
147 pub name: String,
149 pub size: String,
151 pub availability_zone: Option<String>,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct ObjectGrant {
158 pub grantee: String,
160 pub privilege_type: String,
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
169pub enum ClusterConfig {
170 Managed {
172 options: ClusterOptions,
174 grants: Vec<ObjectGrant>,
176 },
177 Unmanaged {
179 replicas: Vec<ClusterReplica>,
181 grants: Vec<ObjectGrant>,
183 },
184}
185
186impl ClusterConfig {
187 pub fn grants(&self) -> &[ObjectGrant] {
189 match self {
190 ClusterConfig::Managed { grants, .. } => grants,
191 ClusterConfig::Unmanaged { grants, .. } => grants,
192 }
193 }
194}
195
196#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct SchemaDeploymentRecord {
202 pub deploy_id: String,
204 pub database: String,
206 pub schema: String,
208 pub deployed_at: DateTime<Utc>,
210 pub deployed_by: String,
212 pub promoted_at: Option<DateTime<Utc>>,
214 pub git_commit: Option<String>,
216 pub kind: DeploymentKind,
218 pub mode: DeploymentMode,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
228pub struct DeploymentObjectRecord {
229 pub deploy_id: String,
231 pub database: String,
233 pub schema: String,
235 pub object: String,
237 pub object_hash: String,
239 pub deployed_at: DateTime<Utc>,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq)]
247pub struct DeploymentMetadata {
248 pub deploy_id: String,
250 pub promoted_at: Option<DateTime<Utc>>,
252 pub mode: DeploymentMode,
254 pub schemas: Vec<SchemaQualifier>,
256}
257
258#[derive(Debug, Clone, PartialEq, Eq)]
264pub struct ConflictRecord {
265 pub database: String,
267 pub schema: String,
269 pub deploy_id: String,
271 pub promoted_at: DateTime<Utc>,
273}
274
275#[derive(Debug, Clone, PartialEq, Eq)]
282pub struct ProductionClusterRecord {
283 pub cluster_name: String,
285 pub database: String,
287 pub schema: String,
289 pub promoted_at: DateTime<Utc>,
291}
292
293#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
297pub struct DeploymentDetails {
298 pub deployed_at: DateTime<Utc>,
300 pub promoted_at: Option<DateTime<Utc>>,
302 pub deployed_by: String,
304 pub git_commit: Option<String>,
306 pub kind: DeploymentKind,
308 pub mode: DeploymentMode,
310 pub schemas: Vec<SchemaQualifier>,
312}
313
314#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
318pub struct StagingDeployment {
319 pub deployed_at: DateTime<Utc>,
321 pub deployed_by: String,
323 pub git_commit: Option<String>,
325 pub kind: DeploymentKind,
327 pub mode: DeploymentMode,
329 pub schemas: Vec<SchemaQualifier>,
331}
332
333#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
337pub struct DeploymentHistoryEntry {
338 pub deploy_id: String,
340 pub promoted_at: DateTime<Utc>,
342 pub deployed_by: String,
344 pub git_commit: Option<String>,
346 pub kind: DeploymentKind,
348 pub schemas: Vec<SchemaQualifier>,
350}
351
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359pub enum ApplyState {
360 NotStarted,
362 PreSwap,
365 PostSwap,
368}
369
370#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
378pub struct ReplacementMvRecord {
379 pub deploy_id: String,
381 pub target_database: String,
383 pub target_schema: String,
385 pub target_name: String,
387 pub replacement_schema: String,
389}
390
391#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
397pub struct PendingStatement {
398 pub deploy_id: String,
400 pub sequence_num: i32,
402 pub database: String,
404 pub schema: String,
406 pub object: String,
408 pub object_hash: String,
410 pub statement_sql: String,
412 pub statement_kind: String,
414 pub executed_at: Option<DateTime<Utc>>,
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[mz_ore::test]
423 fn test_deployment_kind_display() {
424 assert_eq!(DeploymentKind::Tables.to_string(), "tables");
425 assert_eq!(DeploymentKind::Objects.to_string(), "objects");
426 }
427
428 #[mz_ore::test]
429 fn test_deployment_kind_from_str_valid() {
430 assert_eq!(
431 "tables".parse::<DeploymentKind>().unwrap(),
432 DeploymentKind::Tables
433 );
434 assert_eq!(
435 "objects".parse::<DeploymentKind>().unwrap(),
436 DeploymentKind::Objects
437 );
438 }
439
440 #[mz_ore::test]
441 fn test_deployment_kind_from_str_invalid() {
442 let result = "invalid".parse::<DeploymentKind>();
443 assert!(result.is_err());
444 assert_eq!(result.unwrap_err(), "Invalid deployment kind: invalid");
445 }
446
447 #[mz_ore::test]
448 fn test_deployment_kind_roundtrip() {
449 for kind in [
451 DeploymentKind::Tables,
452 DeploymentKind::Objects,
453 DeploymentKind::Sinks,
454 DeploymentKind::Replacement,
455 ] {
456 let s = kind.to_string();
457 let parsed: DeploymentKind = s.parse().unwrap();
458 assert_eq!(kind, parsed);
459 }
460 }
461
462 #[mz_ore::test]
463 fn test_cluster_options_from_cluster_success() {
464 let cluster = Cluster {
465 id: "u1".to_string(),
466 name: "quickstart".to_string(),
467 size: Some("25cc".to_string()),
468 replication_factor: Some(2),
469 auto_scaling_strategy: None,
470 };
471
472 let options = ClusterOptions::from_cluster(&cluster).unwrap();
473 assert_eq!(options.size, "25cc");
474 assert_eq!(options.replication_factor, 2);
475 }
476
477 #[mz_ore::test]
478 fn test_cluster_options_from_cluster_default_replication() {
479 let cluster = Cluster {
480 id: "u1".to_string(),
481 name: "quickstart".to_string(),
482 size: Some("25cc".to_string()),
483 replication_factor: None, auto_scaling_strategy: None,
485 };
486
487 let options = ClusterOptions::from_cluster(&cluster).unwrap();
488 assert_eq!(options.size, "25cc");
489 assert_eq!(options.replication_factor, 1);
490 }
491
492 #[mz_ore::test]
493 fn test_cluster_options_from_cluster_no_size() {
494 let cluster = Cluster {
495 id: "u1".to_string(),
496 name: "unmanaged".to_string(),
497 size: None, replication_factor: Some(1),
499 auto_scaling_strategy: None,
500 };
501
502 let result = ClusterOptions::from_cluster(&cluster);
503 assert!(result.is_err());
504 let err_msg = result.unwrap_err();
505 assert!(err_msg.contains("unmanaged"));
506 assert!(err_msg.contains("has no size"));
507 }
508
509 #[mz_ore::test]
510 fn test_cluster_options_from_cluster_negative_replication() {
511 let cluster = Cluster {
512 id: "u1".to_string(),
513 name: "test".to_string(),
514 size: Some("25cc".to_string()),
515 replication_factor: Some(-1), auto_scaling_strategy: None,
517 };
518
519 let result = ClusterOptions::from_cluster(&cluster);
520 assert!(result.is_err());
521 assert!(result.unwrap_err().contains("Invalid replication_factor"));
522 }
523
524 #[mz_ore::test]
525 fn test_cluster_equality() {
526 let cluster1 = Cluster {
527 id: "u1".to_string(),
528 name: "test".to_string(),
529 size: Some("25cc".to_string()),
530 replication_factor: Some(1),
531 auto_scaling_strategy: None,
532 };
533
534 let cluster2 = Cluster {
535 id: "u1".to_string(),
536 name: "test".to_string(),
537 size: Some("25cc".to_string()),
538 replication_factor: Some(1),
539 auto_scaling_strategy: None,
540 };
541
542 let cluster3 = Cluster {
543 id: "u2".to_string(), name: "test".to_string(),
545 size: Some("25cc".to_string()),
546 replication_factor: Some(1),
547 auto_scaling_strategy: None,
548 };
549
550 assert_eq!(cluster1, cluster2);
551 assert_ne!(cluster1, cluster3);
552 }
553
554 #[mz_ore::test]
555 fn test_cluster_options_equality() {
556 let opts1 = ClusterOptions {
557 size: "25cc".to_string(),
558 replication_factor: 2,
559 auto_scaling_strategy: None,
560 };
561
562 let opts2 = ClusterOptions {
563 size: "25cc".to_string(),
564 replication_factor: 2,
565 auto_scaling_strategy: None,
566 };
567
568 let opts3 = ClusterOptions {
569 size: "50cc".to_string(),
570 replication_factor: 2,
571 auto_scaling_strategy: None,
572 };
573
574 assert_eq!(opts1, opts2);
575 assert_ne!(opts1, opts3);
576 }
577
578 #[mz_ore::test]
579 fn test_cluster_replica_equality() {
580 let r1 = ClusterReplica {
581 name: "r1".to_string(),
582 size: "25cc".to_string(),
583 availability_zone: Some("use1-az1".to_string()),
584 };
585
586 let r2 = ClusterReplica {
587 name: "r1".to_string(),
588 size: "25cc".to_string(),
589 availability_zone: Some("use1-az1".to_string()),
590 };
591
592 let r3 = ClusterReplica {
593 name: "r2".to_string(),
594 size: "25cc".to_string(),
595 availability_zone: None,
596 };
597
598 assert_eq!(r1, r2);
599 assert_ne!(r1, r3);
600 }
601
602 #[mz_ore::test]
603 fn test_cluster_grant_equality() {
604 let g1 = ObjectGrant {
605 grantee: "reader".to_string(),
606 privilege_type: "USAGE".to_string(),
607 };
608
609 let g2 = ObjectGrant {
610 grantee: "reader".to_string(),
611 privilege_type: "USAGE".to_string(),
612 };
613
614 let g3 = ObjectGrant {
615 grantee: "writer".to_string(),
616 privilege_type: "CREATE".to_string(),
617 };
618
619 assert_eq!(g1, g2);
620 assert_ne!(g1, g3);
621 }
622
623 #[mz_ore::test]
624 fn test_cluster_config_managed() {
625 let config = ClusterConfig::Managed {
626 options: ClusterOptions {
627 size: "25cc".to_string(),
628 replication_factor: 2,
629 auto_scaling_strategy: None,
630 },
631 grants: vec![ObjectGrant {
632 grantee: "reader".to_string(),
633 privilege_type: "USAGE".to_string(),
634 }],
635 };
636
637 assert_eq!(config.grants().len(), 1);
638 assert_eq!(config.grants()[0].grantee, "reader");
639 }
640
641 #[mz_ore::test]
642 fn test_cluster_config_unmanaged() {
643 let config = ClusterConfig::Unmanaged {
644 replicas: vec![
645 ClusterReplica {
646 name: "r1".to_string(),
647 size: "25cc".to_string(),
648 availability_zone: None,
649 },
650 ClusterReplica {
651 name: "r2".to_string(),
652 size: "50cc".to_string(),
653 availability_zone: Some("use1-az1".to_string()),
654 },
655 ],
656 grants: vec![],
657 };
658
659 if let ClusterConfig::Unmanaged { replicas, grants } = &config {
660 assert_eq!(replicas.len(), 2);
661 assert_eq!(replicas[0].name, "r1");
662 assert_eq!(replicas[1].availability_zone, Some("use1-az1".to_string()));
663 assert!(grants.is_empty());
664 } else {
665 panic!("Expected Unmanaged config");
666 }
667 }
668
669 #[mz_ore::test]
670 fn test_cluster_config_unmanaged_empty_replicas() {
671 let config = ClusterConfig::Unmanaged {
673 replicas: vec![],
674 grants: vec![ObjectGrant {
675 grantee: "admin".to_string(),
676 privilege_type: "CREATE".to_string(),
677 }],
678 };
679
680 if let ClusterConfig::Unmanaged { replicas, grants } = &config {
681 assert!(replicas.is_empty());
682 assert_eq!(grants.len(), 1);
683 } else {
684 panic!("Expected Unmanaged config");
685 }
686 }
687}