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