1use async_trait::async_trait;
7
8use crate::error::Error;
9
10use super::relationship::Relationship;
11use super::routine::Routine;
12use super::table::Table;
13
14#[derive(Debug, Clone)]
16pub struct TableRow {
17 pub table_schema: String,
18 pub table_name: String,
19 pub table_description: Option<String>,
20 pub is_view: bool,
21 pub insertable: bool,
22 pub updatable: bool,
23 pub deletable: bool,
24 pub readable: bool,
25 pub pk_cols: Vec<String>,
26 pub columns_json: String, }
28
29#[derive(Debug, Clone)]
31pub struct RelationshipRow {
32 pub table_schema: String,
33 pub table_name: String,
34 pub foreign_table_schema: String,
35 pub foreign_table_name: String,
36 pub is_self: bool,
37 pub constraint_name: String,
38 pub cols_and_fcols: Vec<(String, String)>,
39 pub one_to_one: bool,
40}
41
42#[derive(Debug, Clone)]
44pub struct RoutineRow {
45 pub routine_schema: String,
46 pub routine_name: String,
47 pub description: Option<String>,
48 pub params_json: String, pub return_type_json: String, pub volatility: String,
51 pub is_variadic: bool,
52 pub executable: bool,
53}
54
55#[derive(Debug, Clone)]
57pub struct ComputedFieldRow {
58 pub table_schema: String,
59 pub table_name: String,
60 pub function_schema: String,
61 pub function_name: String,
62 pub return_type: String,
63 pub returns_set: bool,
64}
65
66#[cfg_attr(test, mockall::automock)]
71#[async_trait]
72pub trait DbIntrospector: Send + Sync {
73 async fn query_tables(&self, schemas: &[String]) -> Result<Vec<TableRow>, Error>;
75
76 async fn query_relationships(&self) -> Result<Vec<RelationshipRow>, Error>;
78
79 async fn query_routines(&self, schemas: &[String]) -> Result<Vec<RoutineRow>, Error>;
81
82 async fn query_computed_fields(
84 &self,
85 schemas: &[String],
86 ) -> Result<Vec<ComputedFieldRow>, Error>;
87
88 async fn query_timezones(&self) -> Result<Vec<String>, Error>;
90}
91
92pub fn parse_columns_json(json: &str) -> Result<Vec<ColumnJson>, serde_json::Error> {
94 serde_json::from_str(json)
95}
96
97pub fn parse_params_json(json: &str) -> Result<Vec<ParamJson>, serde_json::Error> {
99 serde_json::from_str(json)
100}
101
102pub fn parse_return_type_json(json: &str) -> Result<ReturnTypeJson, serde_json::Error> {
104 serde_json::from_str(json)
105}
106
107#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
109pub struct ColumnJson {
110 pub name: String,
111 pub description: Option<String>,
112 pub nullable: bool,
113 pub data_type: String,
114 pub nominal_type: String,
115 pub max_length: Option<i32>,
116 pub default: Option<String>,
117 #[serde(default)]
118 pub enum_values: Vec<String>,
119 #[serde(default)]
120 pub is_composite: bool,
121 pub composite_type_schema: Option<String>,
122 pub composite_type_name: Option<String>,
123}
124
125#[derive(Debug, Clone, serde::Deserialize)]
127pub struct ParamJson {
128 pub name: String,
129 pub pg_type: String,
130 pub type_max_length: String,
131 pub required: bool,
132 #[serde(default)]
133 pub is_variadic: bool,
134}
135
136#[derive(Debug, Clone, serde::Deserialize)]
138pub struct ReturnTypeJson {
139 pub kind: String, pub type_kind: String, pub type_schema: String,
142 pub type_name: String,
143 #[serde(default)]
144 pub is_alias: bool,
145}
146
147impl TableRow {
149 pub fn into_table(self) -> Result<Table, Error> {
150 use compact_str::CompactString;
151 use indexmap::IndexMap;
152 use smallvec::SmallVec;
153 use std::collections::HashMap;
154 use std::sync::Arc;
155
156 use super::table::Column;
157
158 let columns_data: Vec<ColumnJson> = parse_columns_json(&self.columns_json)
159 .map_err(|e| Error::Internal(format!("Failed to parse columns JSON: {}", e)))?;
160
161 let mut columns = IndexMap::with_capacity(columns_data.len());
162 for col in columns_data {
163 if col.name == "location" {
165 tracing::trace!(
166 "Loading 'location' column - is_composite: {}, data_type: {}, composite_type_schema: {:?}, composite_type_name: {:?}",
167 col.is_composite,
168 col.data_type,
169 col.composite_type_schema,
170 col.composite_type_name
171 );
172 }
173 let column = Column {
174 name: col.name.clone().into(),
175 description: col.description,
176 nullable: col.nullable,
177 data_type: col.data_type.into(),
178 nominal_type: col.nominal_type.into(),
179 max_length: col.max_length,
180 default: col.default,
181 enum_values: col.enum_values.into_iter().collect(),
182 is_composite: col.is_composite,
183 composite_type_schema: col.composite_type_schema.map(|s| s.into()),
184 composite_type_name: col.composite_type_name.map(|s| s.into()),
185 };
186 columns.insert(CompactString::from(col.name), column);
187 }
188
189 Ok(Table {
190 schema: self.table_schema.into(),
191 name: self.table_name.into(),
192 description: self.table_description,
193 is_view: self.is_view,
194 insertable: self.insertable,
195 updatable: self.updatable,
196 deletable: self.deletable,
197 readable: self.readable,
198 pk_cols: self
199 .pk_cols
200 .into_iter()
201 .map(|s| s.into())
202 .collect::<SmallVec<_>>(),
203 columns: Arc::new(columns),
204 computed_fields: HashMap::new(), })
206 }
207}
208
209impl RelationshipRow {
211 pub fn into_relationship(self) -> Relationship {
212 use super::relationship::Cardinality;
213 use crate::types::QualifiedIdentifier;
214
215 let cardinality = if self.one_to_one {
216 Cardinality::O2O {
217 constraint: self.constraint_name.into(),
218 columns: self
219 .cols_and_fcols
220 .into_iter()
221 .map(|(a, b)| (a.into(), b.into()))
222 .collect(),
223 is_parent: false,
224 }
225 } else {
226 Cardinality::M2O {
227 constraint: self.constraint_name.into(),
228 columns: self
229 .cols_and_fcols
230 .into_iter()
231 .map(|(a, b)| (a.into(), b.into()))
232 .collect(),
233 }
234 };
235
236 Relationship {
237 table: QualifiedIdentifier::new(&self.table_schema, &self.table_name),
238 foreign_table: QualifiedIdentifier::new(
239 &self.foreign_table_schema,
240 &self.foreign_table_name,
241 ),
242 is_self: self.is_self,
243 cardinality,
244 table_is_view: false, foreign_table_is_view: false,
246 }
247 }
248}
249
250impl RoutineRow {
252 pub fn into_routine(self) -> Result<Routine, Error> {
253 use super::routine::{PgType, ReturnType, RoutineParam, Volatility};
254 use crate::types::QualifiedIdentifier;
255
256 let params_data: Vec<ParamJson> = parse_params_json(&self.params_json)
257 .map_err(|e| Error::Internal(format!("Failed to parse params JSON: {}", e)))?;
258
259 let return_type_data: ReturnTypeJson = parse_return_type_json(&self.return_type_json)
260 .map_err(|e| Error::Internal(format!("Failed to parse return type JSON: {}", e)))?;
261
262 let params = params_data
263 .into_iter()
264 .map(|p| RoutineParam {
265 name: p.name.into(),
266 pg_type: p.pg_type.into(),
267 type_max_length: p.type_max_length.into(),
268 required: p.required,
269 is_variadic: p.is_variadic,
270 })
271 .collect();
272
273 let pg_type = match return_type_data.type_kind.as_str() {
274 "composite" => PgType::Composite(
275 QualifiedIdentifier::new(
276 &return_type_data.type_schema,
277 &return_type_data.type_name,
278 ),
279 return_type_data.is_alias,
280 ),
281 _ => PgType::Scalar(QualifiedIdentifier::new(
282 &return_type_data.type_schema,
283 &return_type_data.type_name,
284 )),
285 };
286
287 let return_type = match return_type_data.kind.as_str() {
288 "setof" => ReturnType::SetOf(pg_type),
289 _ => ReturnType::Single(pg_type),
290 };
291
292 let volatility = Volatility::parse(&self.volatility).unwrap_or(Volatility::Volatile);
293
294 Ok(Routine {
295 schema: self.routine_schema.into(),
296 name: self.routine_name.into(),
297 description: self.description,
298 params,
299 return_type,
300 volatility,
301 is_variadic: self.is_variadic,
302 executable: self.executable,
303 })
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_parse_columns_json() {
313 let json = r#"[
314 {"name": "id", "description": null, "nullable": false, "data_type": "integer", "nominal_type": "integer", "max_length": null, "default": "nextval('seq')", "enum_values": []},
315 {"name": "name", "description": "User name", "nullable": true, "data_type": "text", "nominal_type": "text", "max_length": null, "default": null, "enum_values": []}
316 ]"#;
317
318 let cols = parse_columns_json(json).unwrap();
319 assert_eq!(cols.len(), 2);
320 assert_eq!(cols[0].name, "id");
321 assert!(!cols[0].nullable);
322 assert_eq!(cols[1].name, "name");
323 assert!(cols[1].nullable);
324 }
325
326 #[test]
327 fn test_parse_params_json() {
328 let json = r#"[
329 {"name": "user_id", "pg_type": "integer", "type_max_length": "integer", "required": true, "is_variadic": false},
330 {"name": "limit", "pg_type": "integer", "type_max_length": "integer", "required": false, "is_variadic": false}
331 ]"#;
332
333 let params = parse_params_json(json).unwrap();
334 assert_eq!(params.len(), 2);
335 assert_eq!(params[0].name, "user_id");
336 assert!(params[0].required);
337 assert_eq!(params[1].name, "limit");
338 assert!(!params[1].required);
339 }
340
341 #[test]
342 fn test_parse_return_type_json_scalar() {
343 let json = r#"{"kind": "single", "type_kind": "scalar", "type_schema": "pg_catalog", "type_name": "integer", "is_alias": false}"#;
344
345 let rt = parse_return_type_json(json).unwrap();
346 assert_eq!(rt.kind, "single");
347 assert_eq!(rt.type_kind, "scalar");
348 assert_eq!(rt.type_name, "integer");
349 }
350
351 #[test]
352 fn test_parse_return_type_json_setof_composite() {
353 let json = r#"{"kind": "setof", "type_kind": "composite", "type_schema": "public", "type_name": "users", "is_alias": false}"#;
354
355 let rt = parse_return_type_json(json).unwrap();
356 assert_eq!(rt.kind, "setof");
357 assert_eq!(rt.type_kind, "composite");
358 assert_eq!(rt.type_name, "users");
359 }
360
361 #[test]
362 fn test_table_row_into_table() {
363 let row = TableRow {
364 table_schema: "public".to_string(),
365 table_name: "users".to_string(),
366 table_description: Some("User table".to_string()),
367 is_view: false,
368 insertable: true,
369 updatable: true,
370 deletable: true,
371 readable: true,
372 pk_cols: vec!["id".to_string()],
373 columns_json: r#"[{"name": "id", "description": null, "nullable": false, "data_type": "integer", "nominal_type": "integer", "max_length": null, "default": null, "enum_values": []}]"#.to_string(),
374 };
375
376 let table = row.into_table().unwrap();
377 assert_eq!(table.schema.as_str(), "public");
378 assert_eq!(table.name.as_str(), "users");
379 assert!(table.has_pk());
380 assert_eq!(table.column_count(), 1);
381 }
382
383 #[test]
384 fn test_relationship_row_into_relationship() {
385 let row = RelationshipRow {
386 table_schema: "public".to_string(),
387 table_name: "posts".to_string(),
388 foreign_table_schema: "public".to_string(),
389 foreign_table_name: "users".to_string(),
390 is_self: false,
391 constraint_name: "fk_posts_user".to_string(),
392 cols_and_fcols: vec![("user_id".to_string(), "id".to_string())],
393 one_to_one: false,
394 };
395
396 let rel = row.into_relationship();
397 assert_eq!(rel.table.name.as_str(), "posts");
398 assert_eq!(rel.foreign_table.name.as_str(), "users");
399 assert!(rel.is_to_one()); assert_eq!(rel.constraint_name(), "fk_posts_user");
401 }
402
403 #[test]
404 fn test_routine_row_into_routine() {
405 let row = RoutineRow {
406 routine_schema: "api".to_string(),
407 routine_name: "get_user".to_string(),
408 description: Some("Get user by ID".to_string()),
409 params_json: r#"[{"name": "user_id", "pg_type": "integer", "type_max_length": "integer", "required": true, "is_variadic": false}]"#.to_string(),
410 return_type_json: r#"{"kind": "setof", "type_kind": "composite", "type_schema": "public", "type_name": "users", "is_alias": false}"#.to_string(),
411 volatility: "s".to_string(),
412 is_variadic: false,
413 executable: true,
414 };
415
416 let routine = row.into_routine().unwrap();
417 assert_eq!(routine.schema.as_str(), "api");
418 assert_eq!(routine.name.as_str(), "get_user");
419 assert!(routine.returns_set());
420 assert!(routine.returns_composite());
421 assert!(routine.is_stable());
422 assert_eq!(routine.param_count(), 1);
423 }
424}