1use reqwest::Client;
2use serde::{Deserialize, Serialize};
3use serde_json::{json, Value};
4use std::time::Duration;
5
6use crate::errors::{SchemaPermissionError, WOWSQLError};
7use crate::models::TableSchema;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ColumnDefinition {
14 pub name: String,
15 #[serde(rename = "type")]
16 pub column_type: String,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub auto_increment: Option<bool>,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub unique: Option<bool>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub not_null: Option<bool>,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub default: Option<String>,
25}
26
27impl ColumnDefinition {
28 pub fn new(name: impl Into<String>, column_type: impl Into<String>) -> Self {
29 Self {
30 name: name.into(),
31 column_type: column_type.into(),
32 auto_increment: None,
33 unique: None,
34 not_null: None,
35 default: None,
36 }
37 }
38
39 pub fn auto_increment(mut self, value: bool) -> Self {
40 self.auto_increment = Some(value);
41 self
42 }
43
44 pub fn unique(mut self, value: bool) -> Self {
45 self.unique = Some(value);
46 self
47 }
48
49 pub fn not_null(mut self, value: bool) -> Self {
50 self.not_null = Some(value);
51 self
52 }
53
54 pub fn default_value(mut self, value: impl Into<String>) -> Self {
55 self.default = Some(value.into());
56 self
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct IndexDefinition {
63 pub name: Option<String>,
64 pub columns: Vec<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub unique: Option<bool>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub using: Option<String>,
69}
70
71impl IndexDefinition {
72 pub fn new(columns: Vec<String>) -> Self {
73 Self {
74 name: None,
75 columns,
76 unique: None,
77 using: None,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Serialize)]
84pub struct CreateTableRequest {
85 pub table_name: String,
86 pub columns: Vec<ColumnDefinition>,
87 pub primary_key: String,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub indexes: Option<Vec<IndexDefinition>>,
90}
91
92fn assert_uuid_primary_key(pk: &str, columns: &[ColumnDefinition]) -> Result<(), WOWSQLError> {
93 let col = columns.iter().find(|c| c.name == pk).ok_or_else(|| {
94 WOWSQLError::new("Primary key column not found in columns", Some(400), None)
95 })?;
96 let first = col
97 .column_type
98 .split_whitespace()
99 .next()
100 .unwrap_or("")
101 .to_uppercase();
102 if first != "UUID" {
103 return Err(WOWSQLError::new(
104 "Primary key column must use PostgreSQL type UUID",
105 Some(400),
106 None,
107 ));
108 }
109 Ok(())
110}
111
112#[derive(Debug, Clone, Serialize)]
114pub struct AlterTableRequest {
115 pub table_name: String,
116 pub operation: String,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub add_columns: Option<Vec<ColumnDefinition>>,
119 #[serde(skip_serializing_if = "Option::is_none")]
120 pub modify_columns: Option<Vec<ColumnDefinition>>,
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub drop_columns: Option<Vec<String>>,
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub rename_columns: Option<Vec<RenameColumn>>,
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub options: Option<Value>,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct RenameColumn {
132 pub old_name: String,
133 pub new_name: String,
134}
135
136#[derive(Debug, Clone, Deserialize)]
138pub struct SchemaResponse {
139 pub success: Option<bool>,
140 pub message: Option<String>,
141 pub table: Option<String>,
142 pub operation: Option<String>,
143}
144
145pub struct SchemaClientBuilder {
149 project_url: String,
150 service_key: String,
151 base_domain: String,
152 secure: bool,
153 timeout: u64,
154 verify_ssl: bool,
155}
156
157impl SchemaClientBuilder {
158 fn new(project_url: &str, service_key: &str) -> Self {
159 Self {
160 project_url: project_url.to_string(),
161 service_key: service_key.to_string(),
162 base_domain: "wowsql.com".to_string(),
163 secure: true,
164 timeout: 30,
165 verify_ssl: true,
166 }
167 }
168
169 pub fn base_domain(mut self, domain: &str) -> Self {
170 self.base_domain = domain.to_string();
171 self
172 }
173
174 pub fn secure(mut self, secure: bool) -> Self {
175 self.secure = secure;
176 self
177 }
178
179 pub fn timeout(mut self, seconds: u64) -> Self {
180 self.timeout = seconds;
181 self
182 }
183
184 pub fn verify_ssl(mut self, verify: bool) -> Self {
185 self.verify_ssl = verify;
186 self
187 }
188
189 pub fn build(self) -> Result<SchemaClient, WOWSQLError> {
190 let base_url = crate::client::WOWSQLClient::build_base_url(
191 &self.project_url,
192 &self.base_domain,
193 self.secure,
194 );
195
196 let client = Client::builder()
197 .timeout(Duration::from_secs(self.timeout))
198 .danger_accept_invalid_certs(!self.verify_ssl)
199 .build()
200 .map_err(|e| {
201 WOWSQLError::new(&format!("Failed to create HTTP client: {}", e), None, None)
202 })?;
203
204 Ok(SchemaClient {
205 base_url,
206 service_key: self.service_key,
207 client,
208 })
209 }
210}
211
212pub struct SchemaClient {
218 base_url: String,
219 service_key: String,
220 client: Client,
221}
222
223impl SchemaClient {
224 pub fn new(
228 project_url: impl Into<String>,
229 service_key: impl Into<String>,
230 ) -> Result<Self, WOWSQLError> {
231 let url: String = project_url.into();
232 let key: String = service_key.into();
233 SchemaClientBuilder::new(&url, &key).build()
234 }
235
236 pub fn builder(project_url: &str, service_key: &str) -> SchemaClientBuilder {
238 SchemaClientBuilder::new(project_url, service_key)
239 }
240
241 pub async fn create_table(
246 &self,
247 name: &str,
248 columns: Vec<ColumnDefinition>,
249 primary_key: &str,
250 indexes: Option<Vec<IndexDefinition>>,
251 ) -> Result<SchemaResponse, WOWSQLError> {
252 assert_uuid_primary_key(primary_key, &columns)?;
253 let url = format!("{}/api/v2/schema/tables", self.base_url);
254 let request = CreateTableRequest {
255 table_name: name.to_string(),
256 columns,
257 primary_key: primary_key.to_string(),
258 indexes,
259 };
260 self.execute_schema_request(&url, "POST", Some(serde_json::to_value(&request).unwrap()))
261 .await
262 }
263
264 pub async fn alter_table(
266 &self,
267 name: &str,
268 operation: &str,
269 options: Option<Value>,
270 ) -> Result<SchemaResponse, WOWSQLError> {
271 let url = format!("{}/api/v2/schema/tables/{}", self.base_url, name);
272 let mut body = json!({
273 "table_name": name,
274 "operation": operation,
275 });
276 if let Some(Value::Object(map)) = options {
277 for (k, v) in map {
278 body[k] = v;
279 }
280 }
281 self.execute_schema_request(&url, "PATCH", Some(body)).await
282 }
283
284 pub async fn drop_table(
286 &self,
287 name: &str,
288 cascade: Option<bool>,
289 ) -> Result<SchemaResponse, WOWSQLError> {
290 let cascade = cascade.unwrap_or(false);
291 let url = format!(
292 "{}/api/v2/schema/tables/{}?cascade={}",
293 self.base_url, name, cascade
294 );
295 self.execute_schema_request(&url, "DELETE", None).await
296 }
297
298 pub async fn execute_sql(&self, sql: &str) -> Result<SchemaResponse, WOWSQLError> {
300 let url = format!("{}/api/v2/schema/execute", self.base_url);
301 let payload = json!({ "sql": sql });
302 self.execute_schema_request(&url, "POST", Some(payload))
303 .await
304 }
305
306 pub async fn add_column(
310 &self,
311 table: &str,
312 column: ColumnDefinition,
313 ) -> Result<SchemaResponse, WOWSQLError> {
314 self.alter_table(
315 table,
316 "add_column",
317 Some(json!({ "add_columns": [column] })),
318 )
319 .await
320 }
321
322 pub async fn drop_column(
324 &self,
325 table: &str,
326 column_name: &str,
327 ) -> Result<SchemaResponse, WOWSQLError> {
328 self.alter_table(
329 table,
330 "drop_column",
331 Some(json!({ "drop_columns": [column_name] })),
332 )
333 .await
334 }
335
336 pub async fn rename_column(
338 &self,
339 table: &str,
340 old_name: &str,
341 new_name: &str,
342 ) -> Result<SchemaResponse, WOWSQLError> {
343 self.alter_table(
344 table,
345 "rename_column",
346 Some(json!({
347 "rename_columns": [{
348 "old_name": old_name,
349 "new_name": new_name
350 }]
351 })),
352 )
353 .await
354 }
355
356 pub async fn modify_column(
358 &self,
359 table: &str,
360 column: ColumnDefinition,
361 ) -> Result<SchemaResponse, WOWSQLError> {
362 self.alter_table(
363 table,
364 "modify_column",
365 Some(json!({ "modify_columns": [column] })),
366 )
367 .await
368 }
369
370 pub async fn create_index(
374 &self,
375 table: &str,
376 columns: Vec<String>,
377 unique: Option<bool>,
378 name: Option<&str>,
379 using: Option<&str>,
380 ) -> Result<SchemaResponse, WOWSQLError> {
381 let idx_name = name
382 .map(|s| s.to_string())
383 .unwrap_or_else(|| format!("idx_{}_{}", table, columns.join("_")));
384
385 let unique = unique.unwrap_or(false);
386 let unique_str = if unique { "UNIQUE " } else { "" };
387 let using_str = using.map(|u| format!(" USING {}", u)).unwrap_or_default();
388
389 let sql = format!(
390 "CREATE {}INDEX {} ON {}{} ({})",
391 unique_str,
392 idx_name,
393 table,
394 using_str,
395 columns.join(", ")
396 );
397
398 self.execute_sql(&sql).await
399 }
400
401 pub async fn list_tables(&self) -> Result<Vec<String>, WOWSQLError> {
405 let url = format!("{}/api/v2/schema/tables", self.base_url);
406 let response: Value = self.execute_json_request(&url, "GET", None).await?;
407
408 if let Some(tables) = response.get("tables").and_then(|v| v.as_array()) {
409 return Ok(tables
410 .iter()
411 .filter_map(|v| v.as_str().map(|s| s.to_string()))
412 .collect());
413 }
414 Ok(vec![])
415 }
416
417 pub async fn get_table_schema(&self, table: &str) -> Result<TableSchema, WOWSQLError> {
419 let url = format!("{}/api/v2/schema/tables/{}", self.base_url, table);
420 self.execute_json_request(&url, "GET", None).await
421 }
422
423 async fn execute_schema_request(
426 &self,
427 url: &str,
428 method: &str,
429 body: Option<Value>,
430 ) -> Result<SchemaResponse, WOWSQLError> {
431 let mut request = self
432 .client
433 .request(
434 method
435 .parse()
436 .map_err(|_| WOWSQLError::new("Invalid method", None, None))?,
437 url,
438 )
439 .header("Content-Type", "application/json")
440 .header("Authorization", format!("Bearer {}", self.service_key));
441
442 if let Some(body) = body {
443 request = request.json(&body);
444 }
445
446 let response = request.send().await?;
447 let status = response.status();
448
449 if status.as_u16() == 403 {
450 return Err(SchemaPermissionError::new(
451 "Schema operations require a SERVICE ROLE key. \
452 You are using an anonymous key which cannot modify database schema.",
453 )
454 .into());
455 }
456
457 let text = response.text().await?;
458
459 if !status.is_success() {
460 return Err(WOWSQLError::from_response(status.as_u16(), &text));
461 }
462
463 serde_json::from_str(&text)
464 .map_err(|e| WOWSQLError::new(&format!("Failed to parse response: {}", e), None, None))
465 }
466
467 async fn execute_json_request<T: serde::de::DeserializeOwned>(
468 &self,
469 url: &str,
470 method: &str,
471 body: Option<Value>,
472 ) -> Result<T, WOWSQLError> {
473 let mut request = self
474 .client
475 .request(
476 method
477 .parse()
478 .map_err(|_| WOWSQLError::new("Invalid method", None, None))?,
479 url,
480 )
481 .header("Content-Type", "application/json")
482 .header("Authorization", format!("Bearer {}", self.service_key));
483
484 if let Some(body) = body {
485 request = request.json(&body);
486 }
487
488 let response = request.send().await?;
489 let status = response.status();
490
491 if status.as_u16() == 403 {
492 return Err(SchemaPermissionError::new(
493 "Schema operations require a SERVICE ROLE key.",
494 )
495 .into());
496 }
497
498 let text = response.text().await?;
499
500 if !status.is_success() {
501 return Err(WOWSQLError::from_response(status.as_u16(), &text));
502 }
503
504 serde_json::from_str(&text)
505 .map_err(|e| WOWSQLError::new(&format!("Failed to parse response: {}", e), None, None))
506 }
507}