use serde::{Deserialize, Serialize};
use tokio_postgres::{Client, Error as PgDriverError};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DbSummary {
pub name: String,
pub is_template: bool,
pub owner: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaSummary {
pub name: String,
pub owner: String,
pub is_system: bool,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum RelationKind {
Table,
View,
MaterializedView,
PartitionedTable,
ForeignTable,
}
impl RelationKind {
fn from_relkind(c: i8) -> Option<Self> {
match c as u8 as char {
'r' => Some(Self::Table),
'v' => Some(Self::View),
'm' => Some(Self::MaterializedView),
'p' => Some(Self::PartitionedTable),
'f' => Some(Self::ForeignTable),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Relation {
pub schema: String,
pub name: String,
pub kind: RelationKind,
pub owner: String,
pub estimated_rows: f32,
}
pub async fn list_databases(client: &Client) -> Result<Vec<DbSummary>, PgDriverError> {
let rows = client
.query(
"SELECT d.datname,
d.datistemplate,
pg_catalog.pg_get_userbyid(d.datdba) AS owner
FROM pg_catalog.pg_database d
WHERE NOT d.datistemplate
ORDER BY d.datname",
&[],
)
.await?;
Ok(rows
.into_iter()
.map(|r| DbSummary {
name: r.get(0),
is_template: r.get(1),
owner: r.get(2),
})
.collect())
}
pub async fn list_schemas(client: &Client) -> Result<Vec<SchemaSummary>, PgDriverError> {
let rows = client
.query(
"SELECT n.nspname,
pg_catalog.pg_get_userbyid(n.nspowner) AS owner,
(n.nspname IN ('pg_catalog', 'information_schema', 'pg_toast')
OR n.nspname LIKE 'pg_temp_%'
OR n.nspname LIKE 'pg_toast_temp_%') AS is_system
FROM pg_catalog.pg_namespace n
ORDER BY is_system, n.nspname",
&[],
)
.await?;
Ok(rows
.into_iter()
.map(|r| SchemaSummary {
name: r.get(0),
owner: r.get(1),
is_system: r.get(2),
})
.collect())
}
pub async fn list_relations(client: &Client, schema: &str) -> Result<Vec<Relation>, PgDriverError> {
let rows = client
.query(
"SELECT n.nspname,
c.relname,
c.relkind,
pg_catalog.pg_get_userbyid(c.relowner) AS owner,
c.reltuples
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = $1
AND c.relkind = ANY(ARRAY['r','v','m','p','f']::\"char\"[])
ORDER BY c.relname",
&[&schema],
)
.await?;
Ok(rows
.into_iter()
.filter_map(|r| {
let relkind: i8 = r.get(2);
RelationKind::from_relkind(relkind).map(|kind| Relation {
schema: r.get(0),
name: r.get(1),
kind,
owner: r.get(3),
estimated_rows: r.get(4),
})
})
.collect())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Sequence {
pub schema: String,
pub name: String,
pub owner: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum RoutineKind {
Function,
Procedure,
Aggregate,
Window,
}
impl RoutineKind {
fn from_prokind(c: i8) -> Option<Self> {
match c as u8 as char {
'f' => Some(Self::Function),
'p' => Some(Self::Procedure),
'a' => Some(Self::Aggregate),
'w' => Some(Self::Window),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Routine {
pub schema: String,
pub name: String,
pub kind: RoutineKind,
pub owner: String,
pub argument_signature: String,
pub return_type: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ObjectTypeKind {
Composite,
Enum,
Domain,
Range,
}
impl ObjectTypeKind {
fn from_typtype(c: i8) -> Option<Self> {
match c as u8 as char {
'c' => Some(Self::Composite),
'e' => Some(Self::Enum),
'd' => Some(Self::Domain),
'r' | 'm' => Some(Self::Range),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObjectType {
pub schema: String,
pub name: String,
pub kind: ObjectTypeKind,
pub owner: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaContents {
pub tables: Vec<Relation>,
pub views: Vec<Relation>,
pub materialized_views: Vec<Relation>,
pub sequences: Vec<Sequence>,
pub routines: Vec<Routine>,
pub object_types: Vec<ObjectType>,
}
pub async fn list_sequences(client: &Client, schema: &str) -> Result<Vec<Sequence>, PgDriverError> {
let rows = client
.query(
"SELECT n.nspname,
c.relname,
pg_catalog.pg_get_userbyid(c.relowner) AS owner
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = $1
AND c.relkind = 'S'
ORDER BY c.relname",
&[&schema],
)
.await?;
Ok(rows
.into_iter()
.map(|r| Sequence {
schema: r.get(0),
name: r.get(1),
owner: r.get(2),
})
.collect())
}
pub async fn list_routines(client: &Client, schema: &str) -> Result<Vec<Routine>, PgDriverError> {
let rows = client
.query(
"SELECT n.nspname,
p.proname,
p.prokind,
pg_catalog.pg_get_userbyid(p.proowner) AS owner,
pg_catalog.pg_get_function_identity_arguments(p.oid) AS arg_sig,
pg_catalog.pg_get_function_result(p.oid) AS ret_type
FROM pg_catalog.pg_proc p
JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = $1
ORDER BY p.proname, arg_sig",
&[&schema],
)
.await?;
Ok(rows
.into_iter()
.filter_map(|r| {
let prokind: i8 = r.get(2);
RoutineKind::from_prokind(prokind).map(|kind| Routine {
schema: r.get(0),
name: r.get(1),
kind,
owner: r.get(3),
argument_signature: r.get::<_, Option<String>>(4).unwrap_or_default(),
return_type: r.get::<_, Option<String>>(5),
})
})
.collect())
}
pub async fn list_object_types(
client: &Client,
schema: &str,
) -> Result<Vec<ObjectType>, PgDriverError> {
let rows = client
.query(
"SELECT n.nspname,
t.typname,
t.typtype,
pg_catalog.pg_get_userbyid(t.typowner) AS owner
FROM pg_catalog.pg_type t
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
LEFT JOIN pg_catalog.pg_class c ON c.oid = t.typrelid
WHERE n.nspname = $1
AND t.typtype IN ('c', 'e', 'd', 'r', 'm')
AND (t.typrelid = 0 OR c.relkind = 'c')
AND NOT EXISTS (
-- Exclude array element type variants — pg
-- generates `_int4`, `_text`, etc. for every
-- type. They show up as composite-of-the-base
-- which is not user-meaningful in this view.
SELECT 1 FROM pg_catalog.pg_type elem
WHERE elem.typarray = t.oid
)
ORDER BY t.typname",
&[&schema],
)
.await?;
Ok(rows
.into_iter()
.filter_map(|r| {
let typtype: i8 = r.get(2);
ObjectTypeKind::from_typtype(typtype).map(|kind| ObjectType {
schema: r.get(0),
name: r.get(1),
kind,
owner: r.get(3),
})
})
.collect())
}
pub async fn list_schema_contents(
client: &Client,
schema: &str,
) -> Result<SchemaContents, PgDriverError> {
let (relations, sequences, routines, object_types) = tokio::try_join!(
list_relations(client, schema),
list_sequences(client, schema),
list_routines(client, schema),
list_object_types(client, schema),
)?;
let mut tables = Vec::new();
let mut views = Vec::new();
let mut materialized_views = Vec::new();
for r in relations {
match r.kind {
RelationKind::Table | RelationKind::PartitionedTable | RelationKind::ForeignTable => {
tables.push(r);
}
RelationKind::View => views.push(r),
RelationKind::MaterializedView => materialized_views.push(r),
}
}
Ok(SchemaContents {
tables,
views,
materialized_views,
sequences,
routines,
object_types,
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColumnDetail {
pub name: String,
pub type_name: String,
pub not_null: bool,
pub has_default: bool,
pub is_generated: bool,
}
pub async fn describe_columns(
client: &Client,
schema: &str,
table: &str,
) -> Result<Vec<ColumnDetail>, PgDriverError> {
let rows = client
.query(
"SELECT a.attname,
pg_catalog.format_type(a.atttypid, NULL) AS type_name,
a.attnotnull,
(d.adbin IS NOT NULL) AS has_default,
(a.attgenerated <> '') AS is_generated
FROM pg_catalog.pg_attribute a
JOIN pg_catalog.pg_class c ON c.oid = a.attrelid
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
LEFT JOIN pg_catalog.pg_attrdef d
ON d.adrelid = a.attrelid AND d.adnum = a.attnum
WHERE n.nspname = $1
AND c.relname = $2
AND a.attnum > 0
AND NOT a.attisdropped
ORDER BY a.attnum",
&[&schema, &table],
)
.await?;
Ok(rows
.into_iter()
.map(|r| ColumnDetail {
name: r.get(0),
type_name: r.get(1),
not_null: r.get(2),
has_default: r.get(3),
is_generated: r.get(4),
})
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn relation_kind_decodes_known_relkinds() {
assert_eq!(
RelationKind::from_relkind(b'r' as i8),
Some(RelationKind::Table)
);
assert_eq!(
RelationKind::from_relkind(b'v' as i8),
Some(RelationKind::View)
);
assert_eq!(
RelationKind::from_relkind(b'm' as i8),
Some(RelationKind::MaterializedView)
);
assert_eq!(
RelationKind::from_relkind(b'p' as i8),
Some(RelationKind::PartitionedTable)
);
assert_eq!(
RelationKind::from_relkind(b'f' as i8),
Some(RelationKind::ForeignTable)
);
}
#[test]
fn relation_kind_rejects_unknown() {
assert_eq!(RelationKind::from_relkind(b'i' as i8), None);
assert_eq!(RelationKind::from_relkind(b'S' as i8), None);
assert_eq!(RelationKind::from_relkind(b't' as i8), None);
assert_eq!(RelationKind::from_relkind(b'c' as i8), None);
}
#[test]
fn routine_kind_decodes_known_prokinds() {
assert_eq!(
RoutineKind::from_prokind(b'f' as i8),
Some(RoutineKind::Function)
);
assert_eq!(
RoutineKind::from_prokind(b'p' as i8),
Some(RoutineKind::Procedure)
);
assert_eq!(
RoutineKind::from_prokind(b'a' as i8),
Some(RoutineKind::Aggregate)
);
assert_eq!(
RoutineKind::from_prokind(b'w' as i8),
Some(RoutineKind::Window)
);
assert_eq!(RoutineKind::from_prokind(b'?' as i8), None);
}
#[test]
fn object_type_kind_decodes_known_typtypes() {
assert_eq!(
ObjectTypeKind::from_typtype(b'c' as i8),
Some(ObjectTypeKind::Composite)
);
assert_eq!(
ObjectTypeKind::from_typtype(b'e' as i8),
Some(ObjectTypeKind::Enum)
);
assert_eq!(
ObjectTypeKind::from_typtype(b'd' as i8),
Some(ObjectTypeKind::Domain)
);
assert_eq!(
ObjectTypeKind::from_typtype(b'r' as i8),
Some(ObjectTypeKind::Range)
);
assert_eq!(
ObjectTypeKind::from_typtype(b'm' as i8),
Some(ObjectTypeKind::Range)
);
assert_eq!(ObjectTypeKind::from_typtype(b'b' as i8), None);
}
}