dbmcp_postgres/tools/
list_procedures.rs1use dbmcp_server::pagination::{Cursor, Pager};
4
5use super::prelude::*;
6use crate::types::{ListEntriesResponse, ListProceduresRequest};
7
8const NAME: &str = "listProcedures";
9const TITLE: &str = "List Procedures";
10const DESCRIPTION_PINNED: &str = include_str!("../../assets/tools/list_procedures/pinned.md");
11const DESCRIPTION_UNPINNED: &str = include_str!("../../assets/tools/list_procedures/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 PinnedListProceduresTool;
23
24impl ToolBase for PinnedListProceduresTool {
25 type Parameter = ListProceduresRequest;
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 PinnedListProceduresTool {
55 async fn invoke(handler: &PostgresHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
56 handler
57 .list_procedures(None, params.cursor, params.search, params.detailed)
58 .await
59 }
60}
61
62pub(crate) struct UnpinnedListProceduresTool;
64
65impl ToolBase for UnpinnedListProceduresTool {
66 type Parameter = ListProceduresRequest;
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 UnpinnedListProceduresTool {
96 async fn invoke(handler: &PostgresHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
97 handler
98 .list_procedures(params.database, params.cursor, params.search, params.detailed)
99 .await
100 }
101}
102
103const BRIEF_SQL: &str = r"
111 SELECT p.proname
112 FROM pg_proc p
113 JOIN pg_namespace n ON n.oid = p.pronamespace
114 WHERE n.nspname = 'public'
115 AND p.prokind = 'p'
116 AND ($1::text IS NULL OR p.proname ILIKE '%' || $1 || '%')
117 ORDER BY p.proname, p.oid
118 LIMIT $2 OFFSET $3";
119
120const DETAILED_SQL: &str = r"
136 SELECT
137 p.proname || '(' || args.text || ')' AS name,
138 json_build_object(
139 'schema', 'public',
140 'name', p.proname,
141 'language', l.lanname,
142 'arguments', args.text,
143 'security', CASE WHEN p.prosecdef THEN 'DEFINER' ELSE 'INVOKER' END,
144 'owner', r.rolname,
145 'description', pg_catalog.obj_description(p.oid, 'pg_proc'),
146 'definition', pg_get_functiondef(p.oid)
147 ) AS entry
148 FROM pg_proc p
149 JOIN pg_namespace n ON n.oid = p.pronamespace
150 JOIN pg_language l ON l.oid = p.prolang
151 JOIN pg_roles r ON r.oid = p.proowner
152 CROSS JOIN LATERAL (SELECT pg_get_function_arguments(p.oid) AS text) args
153 WHERE n.nspname = 'public'
154 AND p.prokind = 'p'
155 AND ($1::text IS NULL OR p.proname ILIKE '%' || $1 || '%')
156 ORDER BY p.proname, p.oid
157 LIMIT $2 OFFSET $3";
158
159impl PostgresHandler {
160 pub async fn list_procedures(
167 &self,
168 database: Option<String>,
169 cursor: Option<Cursor>,
170 search: Option<String>,
171 detailed: bool,
172 ) -> Result<ListEntriesResponse, ErrorData> {
173 let database = database.as_deref().map(str::trim).filter(|s| !s.is_empty());
174 let pattern = search.as_deref().map(str::trim).filter(|s| !s.is_empty());
175 let pager = Pager::new(cursor, self.config.page_size);
176
177 if detailed {
178 let rows: Vec<(String, sqlx::types::Json<serde_json::Value>)> = self
179 .connection
180 .fetch(
181 sqlx::query(DETAILED_SQL)
182 .bind(pattern)
183 .bind(pager.limit())
184 .bind(pager.offset()),
185 database,
186 )
187 .await?;
188 let (rows, next_cursor) = pager.paginate(rows);
189 return Ok(ListEntriesResponse::detailed(
190 rows.into_iter().map(|(key, json)| (key, json.0)).collect(),
191 next_cursor,
192 ));
193 }
194
195 let rows: Vec<String> = self
196 .connection
197 .fetch_scalar(
198 sqlx::query(BRIEF_SQL)
199 .bind(pattern)
200 .bind(pager.limit())
201 .bind(pager.offset()),
202 database,
203 )
204 .await?;
205 let (procedures, next_cursor) = pager.paginate(rows);
206 Ok(ListEntriesResponse::brief(procedures, next_cursor))
207 }
208}