1use std::borrow::Cow;
4
5use dbmcp_server::pagination::{Cursor, Pager};
6use dbmcp_sql::Connection as _;
7
8use crate::types::{ListTablesResponse, PinnedListTablesRequest, UnpinnedListTablesRequest};
9use rmcp::handler::server::router::tool::{AsyncTool, ToolBase};
10use rmcp::model::{ErrorData, ToolAnnotations};
11
12use crate::PostgresHandler;
13
14const NAME: &str = "listTables";
15const TITLE: &str = "List Tables";
16const DESCRIPTION_PINNED: &str = include_str!("../../assets/tools/list_tables/pinned.md");
17const DESCRIPTION_UNPINNED: &str = include_str!("../../assets/tools/list_tables/unpinned.md");
18
19fn annotations() -> ToolAnnotations {
20 ToolAnnotations::new()
21 .read_only(true)
22 .destructive(false)
23 .idempotent(true)
24 .open_world(false)
25}
26
27pub(crate) struct PinnedListTablesTool;
29
30impl ToolBase for PinnedListTablesTool {
31 type Parameter = PinnedListTablesRequest;
32 type Output = ListTablesResponse;
33 type Error = ErrorData;
34
35 fn name() -> Cow<'static, str> {
36 NAME.into()
37 }
38
39 fn title() -> Option<String> {
40 Some(TITLE.into())
41 }
42
43 fn description() -> Option<Cow<'static, str>> {
44 Some(DESCRIPTION_PINNED.into())
45 }
46
47 fn annotations() -> Option<ToolAnnotations> {
48 Some(annotations())
49 }
50}
51
52impl AsyncTool<PostgresHandler> for PinnedListTablesTool {
53 async fn invoke(handler: &PostgresHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
54 handler
55 .list_tables(None, params.cursor, params.search, params.detailed)
56 .await
57 }
58}
59
60pub(crate) struct UnpinnedListTablesTool;
62
63impl ToolBase for UnpinnedListTablesTool {
64 type Parameter = UnpinnedListTablesRequest;
65 type Output = ListTablesResponse;
66 type Error = ErrorData;
67
68 fn name() -> Cow<'static, str> {
69 NAME.into()
70 }
71
72 fn title() -> Option<String> {
73 Some(TITLE.into())
74 }
75
76 fn description() -> Option<Cow<'static, str>> {
77 Some(DESCRIPTION_UNPINNED.into())
78 }
79
80 fn annotations() -> Option<ToolAnnotations> {
81 Some(annotations())
82 }
83}
84
85impl AsyncTool<PostgresHandler> for UnpinnedListTablesTool {
86 async fn invoke(handler: &PostgresHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
87 handler
88 .list_tables(
89 params.database,
90 params.inner.cursor,
91 params.inner.search,
92 params.inner.detailed,
93 )
94 .await
95 }
96}
97
98const BRIEF_SQL: &str = r"
100 SELECT tablename
101 FROM pg_tables
102 WHERE schemaname = 'public'
103 AND ($1::text IS NULL OR tablename ILIKE '%' || $1 || '%')
104 ORDER BY tablename
105 LIMIT $2 OFFSET $3";
106
107const DETAILED_SQL: &str = r"
112 WITH table_info AS (
113 SELECT
114 t.oid AS table_oid,
115 ns.nspname AS schema_name,
116 t.relname AS table_name,
117 pg_get_userbyid(t.relowner) AS table_owner,
118 obj_description(t.oid, 'pg_class') AS table_comment,
119 t.relkind AS object_kind
120 FROM pg_class t
121 JOIN pg_namespace ns ON ns.oid = t.relnamespace
122 WHERE t.relkind IN ('r', 'p')
123 AND ns.nspname = 'public'
124 AND ($1::text IS NULL OR t.relname ILIKE '%' || $1 || '%')
125 ORDER BY t.relname
126 LIMIT $2 OFFSET $3
127 ),
128 columns_info AS (
129 SELECT
130 att.attrelid AS table_oid,
131 att.attname AS column_name,
132 format_type(att.atttypid, att.atttypmod) AS data_type,
133 att.attnum AS ordinal_position,
134 NOT att.attnotnull AS nullable,
135 pg_get_expr(ad.adbin, ad.adrelid) AS column_default,
136 col_description(att.attrelid, att.attnum) AS column_comment
137 FROM pg_attribute att
138 LEFT JOIN pg_attrdef ad ON att.attrelid = ad.adrelid AND att.attnum = ad.adnum
139 JOIN table_info ti ON att.attrelid = ti.table_oid
140 WHERE att.attnum > 0 AND NOT att.attisdropped
141 ),
142 constraints_info AS (
143 SELECT
144 con.conrelid AS table_oid,
145 con.conname AS constraint_name,
146 pg_get_constraintdef(con.oid) AS constraint_definition,
147 CASE con.contype
148 WHEN 'p' THEN 'PRIMARY KEY'
149 WHEN 'f' THEN 'FOREIGN KEY'
150 WHEN 'u' THEN 'UNIQUE'
151 WHEN 'c' THEN 'CHECK'
152 ELSE con.contype::text
153 END AS constraint_type,
154 COALESCE((
155 SELECT array_agg(att.attname ORDER BY u.attposition)
156 FROM unnest(con.conkey) WITH ORDINALITY AS u(attnum, attposition)
157 JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = u.attnum
158 ), ARRAY[]::name[]) AS constraint_columns,
159 CASE WHEN con.confrelid <> 0
160 THEN (con.confrelid::regclass)::text
161 ELSE NULL END AS referenced_table,
162 (
163 SELECT array_agg(att.attname ORDER BY u.attposition)
164 FROM unnest(con.confkey) WITH ORDINALITY AS u(attnum, attposition)
165 JOIN pg_attribute att ON att.attrelid = con.confrelid AND att.attnum = u.attnum
166 WHERE con.contype = 'f'
167 ) AS referenced_columns
168 FROM pg_constraint con
169 JOIN table_info ti ON con.conrelid = ti.table_oid
170 WHERE con.contype IN ('p', 'f', 'u', 'c')
171 ),
172 indexes_info AS (
173 SELECT
174 idx.indrelid AS table_oid,
175 ic.relname AS index_name,
176 pg_get_indexdef(idx.indexrelid) AS index_definition,
177 idx.indisunique AS is_unique,
178 idx.indisprimary AS is_primary,
179 am.amname AS index_method,
180 (
181 SELECT array_agg(att.attname ORDER BY u.ord)
182 FROM unnest(idx.indkey::int[]) WITH ORDINALITY AS u(colidx, ord)
183 LEFT JOIN pg_attribute att ON att.attrelid = idx.indrelid AND att.attnum = u.colidx
184 WHERE u.colidx <> 0
185 ) AS index_columns
186 FROM pg_index idx
187 JOIN pg_class ic ON ic.oid = idx.indexrelid
188 JOIN pg_am am ON am.oid = ic.relam
189 JOIN table_info ti ON idx.indrelid = ti.table_oid
190 ),
191 triggers_info AS (
192 SELECT
193 tg.tgrelid AS table_oid,
194 tg.tgname AS trigger_name,
195 pg_get_triggerdef(tg.oid) AS trigger_definition,
196 tg.tgenabled::text AS trigger_enabled
197 FROM pg_trigger tg
198 JOIN table_info ti ON tg.tgrelid = ti.table_oid
199 WHERE NOT tg.tgisinternal
200 )
201 SELECT ti.table_name AS name, json_build_object(
202 'schema', ti.schema_name,
203 'kind', CASE ti.object_kind
204 WHEN 'r' THEN 'TABLE'
205 WHEN 'p' THEN 'PARTITIONED_TABLE'
206 ELSE ti.object_kind::text
207 END,
208 'owner', ti.table_owner,
209 'comment', ti.table_comment,
210 'columns', COALESCE((
211 SELECT json_agg(json_build_object(
212 'name', ci.column_name,
213 'dataType', ci.data_type,
214 'ordinalPosition', ci.ordinal_position,
215 'nullable', ci.nullable,
216 'default', ci.column_default,
217 'comment', ci.column_comment
218 ) ORDER BY ci.ordinal_position)
219 FROM columns_info ci WHERE ci.table_oid = ti.table_oid
220 ), '[]'::json),
221 'constraints', COALESCE((
222 SELECT json_agg(json_build_object(
223 'name', cons.constraint_name,
224 'type', cons.constraint_type,
225 'columns', cons.constraint_columns,
226 'definition', cons.constraint_definition,
227 'referencedTable', cons.referenced_table,
228 'referencedColumns', cons.referenced_columns
229 ))
230 FROM constraints_info cons WHERE cons.table_oid = ti.table_oid
231 ), '[]'::json),
232 'indexes', COALESCE((
233 SELECT json_agg(json_build_object(
234 'name', ii.index_name,
235 'columns', ii.index_columns,
236 'unique', ii.is_unique,
237 'primary', ii.is_primary,
238 'method', ii.index_method,
239 'definition', ii.index_definition
240 ))
241 FROM indexes_info ii WHERE ii.table_oid = ti.table_oid
242 ), '[]'::json),
243 'triggers', COALESCE((
244 SELECT json_agg(json_build_object(
245 'name', tri.trigger_name,
246 'definition', tri.trigger_definition,
247 'enabled', tri.trigger_enabled
248 ))
249 FROM triggers_info tri WHERE tri.table_oid = ti.table_oid
250 ), '[]'::json)
251 ) AS entry
252 FROM table_info ti
253 ORDER BY ti.schema_name, ti.table_name";
254
255impl PostgresHandler {
256 pub async fn list_tables(
264 &self,
265 database: Option<String>,
266 cursor: Option<Cursor>,
267 search: Option<String>,
268 detailed: bool,
269 ) -> Result<ListTablesResponse, ErrorData> {
270 let database = database.as_deref().map(str::trim).filter(|s| !s.is_empty());
273 let pattern = search.as_deref().map(str::trim).filter(|s| !s.is_empty());
274 let pager = Pager::new(cursor, self.config.page_size);
275
276 if detailed {
277 let rows: Vec<(String, sqlx::types::Json<serde_json::Value>)> = self
278 .connection
279 .fetch(
280 sqlx::query(DETAILED_SQL)
281 .bind(pattern)
282 .bind(pager.limit())
283 .bind(pager.offset()),
284 database,
285 )
286 .await?;
287 let (rows, next_cursor) = pager.paginate(rows);
288 return Ok(ListTablesResponse::detailed(
289 rows.into_iter().map(|(name, json)| (name, json.0)).collect(),
290 next_cursor,
291 ));
292 }
293
294 let rows: Vec<String> = self
295 .connection
296 .fetch_scalar(
297 sqlx::query(BRIEF_SQL)
298 .bind(pattern)
299 .bind(pager.limit())
300 .bind(pager.offset()),
301 database,
302 )
303 .await?;
304 let (tables, next_cursor) = pager.paginate(rows);
305 Ok(ListTablesResponse::brief(tables, next_cursor))
306 }
307}