1use serde::{Deserialize, Serialize};
10use tokio_postgres::{Client, Error as PgDriverError};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct DbSummary {
14 pub name: String,
15 pub is_template: bool,
18 pub owner: String,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct SchemaSummary {
23 pub name: String,
24 pub owner: String,
25 pub is_system: bool,
27}
28
29#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
34pub enum RelationKind {
35 Table,
36 View,
37 MaterializedView,
38 PartitionedTable,
39 ForeignTable,
40}
41
42impl RelationKind {
43 fn from_relkind(c: i8) -> Option<Self> {
44 match c as u8 as char {
45 'r' => Some(Self::Table),
46 'v' => Some(Self::View),
47 'm' => Some(Self::MaterializedView),
48 'p' => Some(Self::PartitionedTable),
49 'f' => Some(Self::ForeignTable),
50 _ => None,
51 }
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct Relation {
57 pub schema: String,
58 pub name: String,
59 pub kind: RelationKind,
60 pub owner: String,
61 pub estimated_rows: f32,
64}
65
66pub async fn list_databases(client: &Client) -> Result<Vec<DbSummary>, PgDriverError> {
67 let rows = client
70 .query(
71 "SELECT d.datname,
72 d.datistemplate,
73 pg_catalog.pg_get_userbyid(d.datdba) AS owner
74 FROM pg_catalog.pg_database d
75 WHERE NOT d.datistemplate
76 ORDER BY d.datname",
77 &[],
78 )
79 .await?;
80 Ok(rows
81 .into_iter()
82 .map(|r| DbSummary {
83 name: r.get(0),
84 is_template: r.get(1),
85 owner: r.get(2),
86 })
87 .collect())
88}
89
90pub async fn list_schemas(client: &Client) -> Result<Vec<SchemaSummary>, PgDriverError> {
91 let rows = client
94 .query(
95 "SELECT n.nspname,
96 pg_catalog.pg_get_userbyid(n.nspowner) AS owner,
97 (n.nspname IN ('pg_catalog', 'information_schema', 'pg_toast')
98 OR n.nspname LIKE 'pg_temp_%'
99 OR n.nspname LIKE 'pg_toast_temp_%') AS is_system
100 FROM pg_catalog.pg_namespace n
101 ORDER BY is_system, n.nspname",
102 &[],
103 )
104 .await?;
105 Ok(rows
106 .into_iter()
107 .map(|r| SchemaSummary {
108 name: r.get(0),
109 owner: r.get(1),
110 is_system: r.get(2),
111 })
112 .collect())
113}
114
115pub async fn list_relations(client: &Client, schema: &str) -> Result<Vec<Relation>, PgDriverError> {
116 let rows = client
123 .query(
124 "SELECT n.nspname,
125 c.relname,
126 c.relkind,
127 pg_catalog.pg_get_userbyid(c.relowner) AS owner,
128 c.reltuples
129 FROM pg_catalog.pg_class c
130 JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
131 WHERE n.nspname = $1
132 AND c.relkind = ANY(ARRAY['r','v','m','p','f']::\"char\"[])
133 ORDER BY c.relname",
134 &[&schema],
135 )
136 .await?;
137
138 Ok(rows
139 .into_iter()
140 .filter_map(|r| {
141 let relkind: i8 = r.get(2);
142 RelationKind::from_relkind(relkind).map(|kind| Relation {
143 schema: r.get(0),
144 name: r.get(1),
145 kind,
146 owner: r.get(3),
147 estimated_rows: r.get(4),
148 })
149 })
150 .collect())
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct Sequence {
170 pub schema: String,
171 pub name: String,
172 pub owner: String,
173}
174
175#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
176pub enum RoutineKind {
177 Function,
179 Procedure,
182 Aggregate,
184 Window,
186}
187
188impl RoutineKind {
189 fn from_prokind(c: i8) -> Option<Self> {
190 match c as u8 as char {
191 'f' => Some(Self::Function),
192 'p' => Some(Self::Procedure),
193 'a' => Some(Self::Aggregate),
194 'w' => Some(Self::Window),
195 _ => None,
196 }
197 }
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct Routine {
202 pub schema: String,
203 pub name: String,
204 pub kind: RoutineKind,
205 pub owner: String,
206 pub argument_signature: String,
210 pub return_type: Option<String>,
214}
215
216#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
217pub enum ObjectTypeKind {
218 Composite,
220 Enum,
222 Domain,
224 Range,
226}
227
228impl ObjectTypeKind {
229 fn from_typtype(c: i8) -> Option<Self> {
230 match c as u8 as char {
231 'c' => Some(Self::Composite),
232 'e' => Some(Self::Enum),
233 'd' => Some(Self::Domain),
234 'r' | 'm' => Some(Self::Range),
235 _ => None,
236 }
237 }
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct ObjectType {
242 pub schema: String,
243 pub name: String,
244 pub kind: ObjectTypeKind,
245 pub owner: String,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct SchemaContents {
254 pub tables: Vec<Relation>,
255 pub views: Vec<Relation>,
256 pub materialized_views: Vec<Relation>,
257 pub sequences: Vec<Sequence>,
258 pub routines: Vec<Routine>,
259 pub object_types: Vec<ObjectType>,
260}
261
262pub async fn list_sequences(client: &Client, schema: &str) -> Result<Vec<Sequence>, PgDriverError> {
263 let rows = client
264 .query(
265 "SELECT n.nspname,
266 c.relname,
267 pg_catalog.pg_get_userbyid(c.relowner) AS owner
268 FROM pg_catalog.pg_class c
269 JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
270 WHERE n.nspname = $1
271 AND c.relkind = 'S'
272 ORDER BY c.relname",
273 &[&schema],
274 )
275 .await?;
276 Ok(rows
277 .into_iter()
278 .map(|r| Sequence {
279 schema: r.get(0),
280 name: r.get(1),
281 owner: r.get(2),
282 })
283 .collect())
284}
285
286pub async fn list_routines(client: &Client, schema: &str) -> Result<Vec<Routine>, PgDriverError> {
287 let rows = client
292 .query(
293 "SELECT n.nspname,
294 p.proname,
295 p.prokind,
296 pg_catalog.pg_get_userbyid(p.proowner) AS owner,
297 pg_catalog.pg_get_function_identity_arguments(p.oid) AS arg_sig,
298 pg_catalog.pg_get_function_result(p.oid) AS ret_type
299 FROM pg_catalog.pg_proc p
300 JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
301 WHERE n.nspname = $1
302 ORDER BY p.proname, arg_sig",
303 &[&schema],
304 )
305 .await?;
306 Ok(rows
307 .into_iter()
308 .filter_map(|r| {
309 let prokind: i8 = r.get(2);
310 RoutineKind::from_prokind(prokind).map(|kind| Routine {
311 schema: r.get(0),
312 name: r.get(1),
313 kind,
314 owner: r.get(3),
315 argument_signature: r.get::<_, Option<String>>(4).unwrap_or_default(),
316 return_type: r.get::<_, Option<String>>(5),
317 })
318 })
319 .collect())
320}
321
322pub async fn list_object_types(
323 client: &Client,
324 schema: &str,
325) -> Result<Vec<ObjectType>, PgDriverError> {
326 let rows = client
332 .query(
333 "SELECT n.nspname,
334 t.typname,
335 t.typtype,
336 pg_catalog.pg_get_userbyid(t.typowner) AS owner
337 FROM pg_catalog.pg_type t
338 JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
339 LEFT JOIN pg_catalog.pg_class c ON c.oid = t.typrelid
340 WHERE n.nspname = $1
341 AND t.typtype IN ('c', 'e', 'd', 'r', 'm')
342 AND (t.typrelid = 0 OR c.relkind = 'c')
343 AND NOT EXISTS (
344 -- Exclude array element type variants — pg
345 -- generates `_int4`, `_text`, etc. for every
346 -- type. They show up as composite-of-the-base
347 -- which is not user-meaningful in this view.
348 SELECT 1 FROM pg_catalog.pg_type elem
349 WHERE elem.typarray = t.oid
350 )
351 ORDER BY t.typname",
352 &[&schema],
353 )
354 .await?;
355 Ok(rows
356 .into_iter()
357 .filter_map(|r| {
358 let typtype: i8 = r.get(2);
359 ObjectTypeKind::from_typtype(typtype).map(|kind| ObjectType {
360 schema: r.get(0),
361 name: r.get(1),
362 kind,
363 owner: r.get(3),
364 })
365 })
366 .collect())
367}
368
369pub async fn list_schema_contents(
374 client: &Client,
375 schema: &str,
376) -> Result<SchemaContents, PgDriverError> {
377 let (relations, sequences, routines, object_types) = tokio::try_join!(
378 list_relations(client, schema),
379 list_sequences(client, schema),
380 list_routines(client, schema),
381 list_object_types(client, schema),
382 )?;
383
384 let mut tables = Vec::new();
388 let mut views = Vec::new();
389 let mut materialized_views = Vec::new();
390 for r in relations {
391 match r.kind {
392 RelationKind::Table | RelationKind::PartitionedTable | RelationKind::ForeignTable => {
393 tables.push(r);
394 }
395 RelationKind::View => views.push(r),
396 RelationKind::MaterializedView => materialized_views.push(r),
397 }
398 }
399 Ok(SchemaContents {
400 tables,
401 views,
402 materialized_views,
403 sequences,
404 routines,
405 object_types,
406 })
407}
408
409#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct ColumnDetail {
414 pub name: String,
415 pub type_name: String,
418 pub not_null: bool,
420 pub has_default: bool,
424 pub is_generated: bool,
427}
428
429pub async fn describe_columns(
430 client: &Client,
431 schema: &str,
432 table: &str,
433) -> Result<Vec<ColumnDetail>, PgDriverError> {
434 let rows = client
438 .query(
439 "SELECT a.attname,
440 pg_catalog.format_type(a.atttypid, NULL) AS type_name,
441 a.attnotnull,
442 (d.adbin IS NOT NULL) AS has_default,
443 (a.attgenerated <> '') AS is_generated
444 FROM pg_catalog.pg_attribute a
445 JOIN pg_catalog.pg_class c ON c.oid = a.attrelid
446 JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
447 LEFT JOIN pg_catalog.pg_attrdef d
448 ON d.adrelid = a.attrelid AND d.adnum = a.attnum
449 WHERE n.nspname = $1
450 AND c.relname = $2
451 AND a.attnum > 0
452 AND NOT a.attisdropped
453 ORDER BY a.attnum",
454 &[&schema, &table],
455 )
456 .await?;
457 Ok(rows
458 .into_iter()
459 .map(|r| ColumnDetail {
460 name: r.get(0),
461 type_name: r.get(1),
462 not_null: r.get(2),
463 has_default: r.get(3),
464 is_generated: r.get(4),
465 })
466 .collect())
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 #[test]
474 fn relation_kind_decodes_known_relkinds() {
475 assert_eq!(
476 RelationKind::from_relkind(b'r' as i8),
477 Some(RelationKind::Table)
478 );
479 assert_eq!(
480 RelationKind::from_relkind(b'v' as i8),
481 Some(RelationKind::View)
482 );
483 assert_eq!(
484 RelationKind::from_relkind(b'm' as i8),
485 Some(RelationKind::MaterializedView)
486 );
487 assert_eq!(
488 RelationKind::from_relkind(b'p' as i8),
489 Some(RelationKind::PartitionedTable)
490 );
491 assert_eq!(
492 RelationKind::from_relkind(b'f' as i8),
493 Some(RelationKind::ForeignTable)
494 );
495 }
496
497 #[test]
498 fn relation_kind_rejects_unknown() {
499 assert_eq!(RelationKind::from_relkind(b'i' as i8), None);
501 assert_eq!(RelationKind::from_relkind(b'S' as i8), None);
502 assert_eq!(RelationKind::from_relkind(b't' as i8), None);
503 assert_eq!(RelationKind::from_relkind(b'c' as i8), None);
504 }
505
506 #[test]
507 fn routine_kind_decodes_known_prokinds() {
508 assert_eq!(
509 RoutineKind::from_prokind(b'f' as i8),
510 Some(RoutineKind::Function)
511 );
512 assert_eq!(
513 RoutineKind::from_prokind(b'p' as i8),
514 Some(RoutineKind::Procedure)
515 );
516 assert_eq!(
517 RoutineKind::from_prokind(b'a' as i8),
518 Some(RoutineKind::Aggregate)
519 );
520 assert_eq!(
521 RoutineKind::from_prokind(b'w' as i8),
522 Some(RoutineKind::Window)
523 );
524 assert_eq!(RoutineKind::from_prokind(b'?' as i8), None);
525 }
526
527 #[test]
528 fn object_type_kind_decodes_known_typtypes() {
529 assert_eq!(
530 ObjectTypeKind::from_typtype(b'c' as i8),
531 Some(ObjectTypeKind::Composite)
532 );
533 assert_eq!(
534 ObjectTypeKind::from_typtype(b'e' as i8),
535 Some(ObjectTypeKind::Enum)
536 );
537 assert_eq!(
538 ObjectTypeKind::from_typtype(b'd' as i8),
539 Some(ObjectTypeKind::Domain)
540 );
541 assert_eq!(
542 ObjectTypeKind::from_typtype(b'r' as i8),
543 Some(ObjectTypeKind::Range)
544 );
545 assert_eq!(
547 ObjectTypeKind::from_typtype(b'm' as i8),
548 Some(ObjectTypeKind::Range)
549 );
550 assert_eq!(ObjectTypeKind::from_typtype(b'b' as i8), None);
553 }
554}