1use std::marker::PhantomData;
16
17use fastrace_reqwest::traceparent_headers;
18use reqwest::Method;
19use serde::Deserialize;
20use serde::Serialize;
21use serde::de::DeserializeOwned;
22
23use crate::Client;
24use crate::DataType;
25use crate::Error;
26use crate::ErrorKind;
27use crate::protocol::apply_response_metadata;
28use crate::protocol::decode_http_error;
29
30#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
33#[serde(deny_unknown_fields)]
34pub struct CatalogDocument {
35 #[serde(default)]
36 pub databases: Vec<CatalogDatabase>,
37}
38
39#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
40#[serde(deny_unknown_fields)]
41pub struct CatalogDatabase {
42 pub name: String,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub comment: Option<String>,
45 #[serde(default)]
46 pub schemas: Vec<CatalogSchema>,
47}
48
49#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
50#[serde(deny_unknown_fields)]
51pub struct CatalogSchema {
52 pub name: String,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub comment: Option<String>,
55 #[serde(default)]
56 pub tables: Vec<CatalogTable>,
57 #[serde(default)]
58 pub views: Vec<CatalogView>,
59}
60
61#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
64#[serde(deny_unknown_fields)]
65pub struct CatalogTable {
66 pub name: String,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub comment: Option<String>,
69 #[serde(default)]
70 pub columns: Vec<CatalogColumn>,
71 #[serde(default)]
72 pub partition_by: Vec<String>,
73 #[serde(default)]
74 pub cluster_by: Vec<String>,
75 #[serde(default)]
76 pub distinct: CatalogDistinct,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub retention_days: Option<i32>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub storage: Option<serde_json::Value>,
81}
82
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84#[serde(deny_unknown_fields)]
85pub struct CatalogColumn {
86 pub name: String,
87 pub data_type: DataType,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub expression: Option<String>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub comment: Option<String>,
92 #[serde(default)]
93 pub indexes: Vec<CatalogColumnIndex>,
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
98#[serde(deny_unknown_fields)]
99pub struct CatalogColumnIndex {
100 #[serde(rename = "type")]
101 pub index_type: String,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub analyzer: Option<String>,
104}
105
106#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
107#[serde(deny_unknown_fields)]
108pub struct CatalogDistinct {
109 #[serde(default)]
110 pub on: Vec<String>,
111 #[serde(default)]
112 pub by: Vec<String>,
113}
114
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
116#[serde(deny_unknown_fields)]
117pub struct CatalogView {
118 pub name: String,
119 pub statement: String,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub comment: Option<String>,
122}
123
124#[derive(Debug, Clone, Default)]
126pub struct CatalogOptions {
127 pub dry_run: bool,
128 pub allow_destructive: bool,
129 pub if_match: Option<String>,
131}
132
133#[derive(Debug, Clone)]
134pub struct CatalogSnapshot<T> {
135 pub definition: T,
136 pub etag: String,
137}
138
139#[derive(Debug, Clone, Deserialize)]
141pub struct CatalogResult<T> {
142 pub before: Option<T>,
143 pub after: Option<T>,
144 pub destructive: bool,
145 #[serde(skip)]
147 pub etag: String,
148}
149
150#[derive(Debug, Clone)]
169pub struct Catalog {
170 resource: CatalogResource<CatalogDocument>,
171}
172
173impl Catalog {
174 pub(crate) fn new(client: Client) -> Self {
175 Self {
176 resource: CatalogResource::new(client, &["catalog"]),
177 }
178 }
179
180 pub async fn get(&self) -> Result<CatalogSnapshot<CatalogDocument>, Error> {
181 self.resource.get().await
182 }
183
184 pub async fn apply(
186 &self,
187 definition: &CatalogDocument,
188 options: CatalogOptions,
189 ) -> Result<CatalogResult<CatalogDocument>, Error> {
190 self.resource.apply(definition, options).await
191 }
192
193 pub fn raw(&self) -> CatalogResource<serde_json::Value> {
195 self.resource.raw()
196 }
197
198 pub fn database(&self, database: &str) -> CatalogResource<CatalogDatabase> {
199 CatalogResource::new(
200 self.resource.client.clone(),
201 &["catalog", "databases", database],
202 )
203 }
204 pub fn schema(&self, database: &str, schema: &str) -> CatalogResource<CatalogSchema> {
205 CatalogResource::new(
206 self.resource.client.clone(),
207 &["catalog", "databases", database, "schemas", schema],
208 )
209 }
210 pub fn table(
211 &self,
212 database: &str,
213 schema: &str,
214 table: &str,
215 ) -> CatalogResource<CatalogTable> {
216 CatalogResource::new(
217 self.resource.client.clone(),
218 &[
219 "catalog",
220 "databases",
221 database,
222 "schemas",
223 schema,
224 "tables",
225 table,
226 ],
227 )
228 }
229 pub fn view(&self, database: &str, schema: &str, view: &str) -> CatalogResource<CatalogView> {
230 CatalogResource::new(
231 self.resource.client.clone(),
232 &[
233 "catalog",
234 "databases",
235 database,
236 "schemas",
237 schema,
238 "views",
239 view,
240 ],
241 )
242 }
243}
244
245#[derive(Debug, Clone)]
253pub struct CatalogResource<T> {
254 client: Client,
255 path: Vec<String>,
256 definition: PhantomData<T>,
257}
258
259impl<T> CatalogResource<T> {
260 fn new(client: Client, path: &[&str]) -> Self {
261 Self {
262 client,
263 path: path.iter().map(|s| (*s).to_string()).collect(),
264 definition: PhantomData,
265 }
266 }
267
268 pub fn raw(&self) -> CatalogResource<serde_json::Value> {
269 CatalogResource {
270 client: self.client.clone(),
271 path: self.path.clone(),
272 definition: PhantomData,
273 }
274 }
275
276 async fn request<R: DeserializeOwned>(
277 &self,
278 method: Method,
279 body: Option<Vec<u8>>,
280 options: CatalogOptions,
281 ) -> Result<(R, String), Error> {
282 let mutation = method != Method::GET;
283 let writing = mutation && !options.dry_run;
284 let url = self
285 .client
286 .make_resource_url(&self.path.iter().map(String::as_str).collect::<Vec<_>>())?;
287 let mut request = self
288 .client
289 .request(method.clone(), url)
290 .headers(traceparent_headers());
291 if options.dry_run {
292 request = request.query(&[("dry_run", true)]);
293 }
294 if options.allow_destructive {
295 request = request.query(&[("allow_destructive", true)]);
296 }
297 if let Some(etag) = options.if_match {
298 let etag = reqwest::header::HeaderValue::from_str(&etag).map_err(|e| {
299 Error::new(ErrorKind::ConfigInvalid, "invalid If-Match header").set_source(e)
300 })?;
301 request = request.header(reqwest::header::IF_MATCH, etag);
302 }
303 if let Some(body) = body {
304 request = request
305 .header(reqwest::header::CONTENT_TYPE, "application/json")
306 .body(body);
307 }
308 let mut response_metadata = None;
309 let read_response = async {
310 let response = request.send().await.map_err(|e| {
311 catalog_transport_error(writing, "failed to send catalog request").set_source(e)
312 })?;
313 let status = response.status();
314 let headers = response.headers().clone();
315 response_metadata = Some((status, headers.clone()));
316 let bytes = response.bytes().await.map_err(|e| {
317 apply_response_metadata(
318 catalog_transport_error(writing, "failed to read catalog response")
319 .set_source(e),
320 status,
321 &headers,
322 )
323 })?;
324 Ok::<_, Error>((status, headers, bytes))
325 };
326 let (status, headers, bytes) = if mutation {
327 match tokio::time::timeout(self.client.write_timeout(), read_response).await {
328 Ok(result) => result?,
329 Err(source) => {
330 let error = catalog_transport_error(writing, "catalog request timed out")
331 .set_source(source);
332 return Err(match response_metadata {
333 Some((status, headers)) => apply_response_metadata(error, status, &headers),
334 None => error,
335 });
336 }
337 }
338 } else {
339 read_response.await?
340 };
341 if !status.is_success() {
342 let error = decode_http_error(status, &headers, &bytes, ErrorKind::CatalogFailed);
343 return Err(if writing {
344 if error.code().is_none() {
345 error
346 .set_api_error(Some("catalog_commit_outcome_unknown".to_string()), None)
347 .set_persistent()
348 } else {
349 error.set_persistent()
350 }
351 } else {
352 error
353 });
354 }
355 let etag = headers
356 .get(reqwest::header::ETAG)
357 .and_then(|h| h.to_str().ok())
358 .filter(|s| !s.is_empty())
359 .ok_or_else(|| {
360 apply_response_metadata(
361 catalog_response_error(writing, "catalog response is missing ETag"),
362 status,
363 &headers,
364 )
365 })?
366 .to_string();
367 let value: serde_json::Value = serde_json::from_slice(&bytes).map_err(|e| {
368 apply_response_metadata(
369 catalog_response_error(writing, "invalid catalog response").set_source(e),
370 status,
371 &headers,
372 )
373 })?;
374 let valid_resource = |v: &serde_json::Value| v.is_object() || v.is_null();
375 if !value.is_object()
376 || (mutation
377 && !(value.get("before").is_some_and(valid_resource)
378 && value.get("after").is_some_and(valid_resource)
379 && value
380 .get("destructive")
381 .is_some_and(serde_json::Value::is_boolean)))
382 {
383 return Err(apply_response_metadata(
384 catalog_response_error(writing, "invalid catalog response"),
385 status,
386 &headers,
387 ));
388 }
389 let result = serde_json::from_value(value).map_err(|e| {
390 apply_response_metadata(
391 catalog_response_error(
392 writing,
393 "cannot decode catalog definition; use raw() to retain unsupported fields",
394 )
395 .set_source(e),
396 status,
397 &headers,
398 )
399 })?;
400 Ok((result, etag))
401 }
402}
403
404impl<T: Serialize + DeserializeOwned> CatalogResource<T> {
405 pub async fn get(&self) -> Result<CatalogSnapshot<T>, Error> {
407 let (definition, etag) = self
408 .request(Method::GET, None, CatalogOptions::default())
409 .await?;
410 Ok(CatalogSnapshot { definition, etag })
411 }
412
413 pub async fn apply(
452 &self,
453 definition: &T,
454 options: CatalogOptions,
455 ) -> Result<CatalogResult<T>, Error> {
456 let body = serde_json::to_vec(definition).map_err(|e| {
457 Error::new(
458 ErrorKind::ConfigInvalid,
459 "failed to serialize catalog definition",
460 )
461 .set_source(e)
462 })?;
463 let (mut result, etag): (CatalogResult<T>, _) =
464 self.request(Method::PUT, Some(body), options).await?;
465 result.etag = etag;
466 Ok(result)
467 }
468
469 pub async fn delete(&self, options: CatalogOptions) -> Result<CatalogResult<T>, Error> {
472 if self.path.len() == 1 {
473 return Err(Error::new(
474 ErrorKind::ConfigInvalid,
475 "the root catalog does not support DELETE; apply an empty document instead",
476 ));
477 }
478 let (mut result, etag): (CatalogResult<T>, _) =
479 self.request(Method::DELETE, None, options).await?;
480 result.etag = etag;
481 Ok(result)
482 }
483}
484
485fn catalog_transport_error(writing: bool, message: &str) -> Error {
486 let error = catalog_response_error(writing, message);
487 if writing {
488 error
489 } else {
490 error.set_temporary()
491 }
492}
493
494fn catalog_response_error(writing: bool, message: &str) -> Error {
495 let error = Error::new(ErrorKind::CatalogFailed, message);
496 if writing {
497 error
498 .set_api_error(Some("catalog_commit_outcome_unknown".to_string()), None)
499 .set_persistent()
500 } else {
501 error
502 }
503}