1use crate::cli::CliError;
13use crate::cli::progress;
14use crate::client::{Client, ConnectionError, quote_identifier};
15use crate::config::Settings;
16use crate::log;
17use crate::project::ir::object_id::ObjectId;
18use crate::{info, info_nonl};
19use std::fmt;
20use std::io::{self, Write};
21use std::path::{Path, PathBuf};
22
23#[derive(Debug, Clone, Copy)]
25pub enum ObjectKind {
26 Cluster,
27 Connection,
28 NetworkPolicy,
29 Role,
30 Secret,
31 Source,
32 Table,
33}
34
35impl ObjectKind {
36 fn label(&self) -> &'static str {
37 match self {
38 ObjectKind::Cluster => "cluster",
39 ObjectKind::Connection => "connection",
40 ObjectKind::NetworkPolicy => "network policy",
41 ObjectKind::Role => "role",
42 ObjectKind::Secret => "secret",
43 ObjectKind::Source => "source",
44 ObjectKind::Table => "table",
45 }
46 }
47
48 fn sql_keyword(&self) -> &'static str {
49 match self {
50 ObjectKind::Cluster => "CLUSTER",
51 ObjectKind::Connection => "CONNECTION",
52 ObjectKind::NetworkPolicy => "NETWORK POLICY",
53 ObjectKind::Role => "ROLE",
54 ObjectKind::Secret => "SECRET",
55 ObjectKind::Source => "SOURCE",
56 ObjectKind::Table => "TABLE",
57 }
58 }
59}
60
61#[derive(serde::Serialize)]
62struct DeleteResult {
63 kind: String,
64 name: String,
65 file_removed: String,
66}
67
68impl fmt::Display for DeleteResult {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 write!(
71 f,
72 " \u{2713} Dropped {} '{}' and removed {}",
73 self.kind, self.name, self.file_removed
74 )
75 }
76}
77
78struct DeleteTarget {
80 file_path: PathBuf,
81 drop_sql: String,
82}
83
84impl DeleteTarget {
85 fn resolve(directory: &Path, kind: ObjectKind, name: &str) -> Result<Self, CliError> {
86 let keyword = kind.sql_keyword();
87 let (file_path, drop_sql) = match kind {
88 ObjectKind::Cluster => (
89 directory.join("clusters").join(format!("{}.sql", name)),
90 format!("DROP {} {}", keyword, quote_identifier(name)),
91 ),
92 ObjectKind::NetworkPolicy => (
93 directory
94 .join("network-policies")
95 .join(format!("{}.sql", name)),
96 format!("DROP {} {}", keyword, quote_identifier(name)),
97 ),
98 ObjectKind::Role => (
99 directory.join("roles").join(format!("{}.sql", name)),
100 format!("DROP {} {}", keyword, quote_identifier(name)),
101 ),
102 ObjectKind::Connection
103 | ObjectKind::Secret
104 | ObjectKind::Source
105 | ObjectKind::Table => {
106 let oid = name.parse::<ObjectId>().map_err(CliError::Message)?;
107 (
108 directory
109 .join("models")
110 .join(oid.expect_database())
111 .join(oid.schema())
112 .join(format!("{}.sql", oid.object())),
113 format!(
114 "DROP {} {}.{}.{}",
115 keyword,
116 quote_identifier(oid.expect_database()),
117 quote_identifier(oid.schema()),
118 quote_identifier(oid.object()),
119 ),
120 )
121 }
122 };
123 Ok(Self {
124 file_path,
125 drop_sql,
126 })
127 }
128}
129
130pub async fn run(
135 settings: &Settings,
136 kind: ObjectKind,
137 name: &str,
138 yes: bool,
139) -> Result<(), CliError> {
140 let profile = settings.connection();
141 let directory = &settings.directory;
142 let label = kind.label();
143
144 let target = DeleteTarget::resolve(directory, kind, name)?;
145 if !target.file_path.exists() {
146 return Err(CliError::Message(format!(
147 "'{name}' is not managed by this project (no file at {})",
148 target.file_path.display()
149 )));
150 }
151
152 if log::json_output_enabled() && !yes {
153 return Err(CliError::Message(
154 "--output json requires --yes to skip interactive confirmation".to_string(),
155 ));
156 }
157
158 if !yes {
159 info!(
160 "This will drop {} '{}' from Materialize and remove {}",
161 label,
162 name,
163 target.file_path.display()
164 );
165 info_nonl!("Continue? [y/N] ");
166 io::stdout().flush()?;
167 let mut input = String::new();
168 io::stdin().read_line(&mut input)?;
169 if !input.trim().eq_ignore_ascii_case("y") {
170 info!("Aborted.");
171 return Ok(());
172 }
173 }
174
175 progress::stage_start(&format!("Dropping {} '{}'", label, name));
176 let client = Client::connect_with_profile(profile.clone())
177 .await
178 .map_err(CliError::Connection)?;
179 super::setup::verify(&client, settings.emulator()).await?;
180 let role = super::setup::validate_connection(&client, settings.emulator()).await?;
181 super::setup::require_deployer(role)?;
182
183 client.execute(&target.drop_sql, &[]).await.map_err(|e| {
184 let msg = e.to_string();
185 if msg.contains("depended upon") || msg.contains("depends on") {
186 CliError::Message(format!(
187 "cannot drop {} '{}' because other objects depend on it.\n\
188 Drop the dependent objects first, then retry.",
189 label, name
190 ))
191 } else {
192 CliError::Connection(ConnectionError::Message(format!(
193 "failed to drop {} '{}': {}",
194 label, name, e
195 )))
196 }
197 })?;
198
199 if let Err(e) = std::fs::remove_file(&target.file_path) {
200 progress::warn(&format!(
201 "DROP succeeded but failed to remove {}: {}",
202 target.file_path.display(),
203 e
204 ));
205 return Err(e.into());
206 }
207
208 let result = DeleteResult {
209 kind: label.to_string(),
210 name: name.to_string(),
211 file_removed: target.file_path.display().to_string(),
212 };
213 log::output(&result);
214
215 Ok(())
216}