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