systemprompt_database/admin/
introspection.rs1use std::sync::Arc;
11
12use sqlx::Row;
13use sqlx::postgres::PgPool;
14
15use crate::admin::identifier::SafeIdentifier;
16use crate::error::{DatabaseResult, RepositoryError};
17use crate::models::{ColumnInfo, DatabaseInfo, IndexInfo, TableInfo};
18
19#[derive(Debug)]
20pub struct DatabaseAdminService {
21 pool: Arc<PgPool>,
22}
23
24impl DatabaseAdminService {
25 pub const fn new(pool: Arc<PgPool>) -> Self {
26 Self { pool }
27 }
28
29 pub async fn list_tables(&self) -> DatabaseResult<Vec<TableInfo>> {
30 let rows = sqlx::query(
31 r"
32 SELECT
33 t.table_name as name,
34 COALESCE(s.n_live_tup, 0) as row_count,
35 COALESCE(pg_total_relation_size(quote_ident(t.table_name)::regclass), 0) as size_bytes
36 FROM information_schema.tables t
37 LEFT JOIN pg_stat_user_tables s ON t.table_name = s.relname
38 WHERE t.table_schema = 'public'
39 ORDER BY t.table_name
40 ",
41 )
42 .fetch_all(&*self.pool)
43 .await?;
44
45 let tables = rows
46 .iter()
47 .map(|row| {
48 let name: String = row.get("name");
49 let row_count: i64 = row.get("row_count");
50 let size_bytes: i64 = row.get("size_bytes");
51 TableInfo {
52 name,
53 row_count,
54 size_bytes,
55 columns: vec![],
56 }
57 })
58 .collect();
59
60 Ok(tables)
61 }
62
63 pub async fn describe_table(
64 &self,
65 table_name: &SafeIdentifier,
66 ) -> DatabaseResult<(Vec<ColumnInfo>, i64)> {
67 let rows = sqlx::query(
68 "SELECT column_name, data_type, is_nullable, column_default FROM \
69 information_schema.columns WHERE table_schema = 'public' AND table_name = $1 \
70 ORDER BY ordinal_position",
71 )
72 .bind(table_name.as_str())
73 .fetch_all(&*self.pool)
74 .await?;
75
76 if rows.is_empty() {
77 return Err(RepositoryError::not_found(format!(
78 "Table '{table_name}' not found"
79 )));
80 }
81
82 let pk_rows = sqlx::query(
83 r"
84 SELECT a.attname as column_name
85 FROM pg_index i
86 JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
87 WHERE i.indrelid = $1::regclass AND i.indisprimary
88 ",
89 )
90 .bind(table_name.as_str())
91 .fetch_all(&*self.pool)
92 .await?;
93
94 let pk_columns: Vec<String> = pk_rows
95 .iter()
96 .map(|row| row.get::<String, _>("column_name"))
97 .collect();
98
99 let columns = rows
100 .iter()
101 .map(|row| {
102 let name: String = row.get("column_name");
103 let data_type: String = row.get("data_type");
104 let nullable_str: String = row.get("is_nullable");
105 let nullable = nullable_str.to_uppercase() == "YES";
106 let default: Option<String> = row.get("column_default");
107 let primary_key = pk_columns.contains(&name);
108
109 ColumnInfo {
110 name,
111 data_type,
112 nullable,
113 primary_key,
114 default,
115 }
116 })
117 .collect();
118
119 let row_count = self.count_rows(table_name).await?;
120
121 Ok((columns, row_count))
122 }
123
124 pub async fn list_table_indexes(
125 &self,
126 table_name: &SafeIdentifier,
127 ) -> DatabaseResult<Vec<IndexInfo>> {
128 let rows = sqlx::query(
129 r"
130 SELECT
131 i.relname as index_name,
132 ix.indisunique as is_unique,
133 array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) as columns
134 FROM pg_class t
135 JOIN pg_index ix ON t.oid = ix.indrelid
136 JOIN pg_class i ON i.oid = ix.indexrelid
137 JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
138 WHERE t.relname = $1 AND t.relkind = 'r'
139 GROUP BY i.relname, ix.indisunique
140 ORDER BY i.relname
141 ",
142 )
143 .bind(table_name.as_str())
144 .fetch_all(&*self.pool)
145 .await?;
146
147 let indexes = rows
148 .iter()
149 .map(|row| {
150 let name: String = row.get("index_name");
151 let unique: bool = row.get("is_unique");
152 let columns: Vec<String> = row.get("columns");
153 IndexInfo {
154 name,
155 columns,
156 unique,
157 }
158 })
159 .collect();
160
161 Ok(indexes)
162 }
163
164 pub async fn count_rows(&self, table_name: &SafeIdentifier) -> DatabaseResult<i64> {
165 let quoted_table = table_name.quoted();
166 let count_query = format!("SELECT COUNT(*) as count FROM {quoted_table}");
167 let row_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(count_query))
168 .fetch_one(&*self.pool)
169 .await?;
170
171 Ok(row_count)
172 }
173
174 pub async fn get_database_info(&self) -> DatabaseResult<DatabaseInfo> {
175 let version: String = sqlx::query_scalar("SELECT version()")
176 .fetch_one(&*self.pool)
177 .await?;
178
179 let size: i64 = sqlx::query_scalar("SELECT pg_database_size(current_database())")
180 .fetch_one(&*self.pool)
181 .await?;
182
183 let size = u64::try_from(size).map_err(|_e| {
184 RepositoryError::internal(format!("pg_database_size returned negative value: {size}"))
185 })?;
186
187 let tables = self.list_tables().await?;
188
189 Ok(DatabaseInfo {
190 path: "PostgreSQL".to_owned(),
191 size,
192 version,
193 tables,
194 })
195 }
196}