dbmcp_postgres/tools/
list_triggers.rs1use std::borrow::Cow;
4
5use dbmcp_server::pagination::{Cursor, Pager};
6use dbmcp_sql::Connection as _;
7use rmcp::handler::server::router::tool::{AsyncTool, ToolBase};
8use rmcp::model::{ErrorData, ToolAnnotations};
9
10use crate::PostgresHandler;
11use crate::types::{ListTriggersResponse, PinnedListTriggersRequest, UnpinnedListTriggersRequest};
12
13const NAME: &str = "listTriggers";
14const TITLE: &str = "List Triggers";
15const DESCRIPTION_PINNED: &str = include_str!("../../assets/tools/list_triggers/pinned.md");
16const DESCRIPTION_UNPINNED: &str = include_str!("../../assets/tools/list_triggers/unpinned.md");
17
18fn annotations() -> ToolAnnotations {
19 ToolAnnotations::new()
20 .read_only(true)
21 .destructive(false)
22 .idempotent(true)
23 .open_world(false)
24}
25
26pub(crate) struct PinnedListTriggersTool;
28
29impl ToolBase for PinnedListTriggersTool {
30 type Parameter = PinnedListTriggersRequest;
31 type Output = ListTriggersResponse;
32 type Error = ErrorData;
33
34 fn name() -> Cow<'static, str> {
35 NAME.into()
36 }
37
38 fn title() -> Option<String> {
39 Some(TITLE.into())
40 }
41
42 fn description() -> Option<Cow<'static, str>> {
43 Some(DESCRIPTION_PINNED.into())
44 }
45
46 fn annotations() -> Option<ToolAnnotations> {
47 Some(annotations())
48 }
49}
50
51impl AsyncTool<PostgresHandler> for PinnedListTriggersTool {
52 async fn invoke(handler: &PostgresHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
53 handler
54 .list_triggers(None, params.cursor, params.search, params.detailed)
55 .await
56 }
57}
58
59pub(crate) struct UnpinnedListTriggersTool;
61
62impl ToolBase for UnpinnedListTriggersTool {
63 type Parameter = UnpinnedListTriggersRequest;
64 type Output = ListTriggersResponse;
65 type Error = ErrorData;
66
67 fn name() -> Cow<'static, str> {
68 NAME.into()
69 }
70
71 fn title() -> Option<String> {
72 Some(TITLE.into())
73 }
74
75 fn description() -> Option<Cow<'static, str>> {
76 Some(DESCRIPTION_UNPINNED.into())
77 }
78
79 fn annotations() -> Option<ToolAnnotations> {
80 Some(annotations())
81 }
82}
83
84impl AsyncTool<PostgresHandler> for UnpinnedListTriggersTool {
85 async fn invoke(handler: &PostgresHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
86 handler
87 .list_triggers(
88 params.database,
89 params.inner.cursor,
90 params.inner.search,
91 params.inner.detailed,
92 )
93 .await
94 }
95}
96
97const BRIEF_SQL: &str = r"
106 SELECT t.tgname
107 FROM pg_trigger t
108 JOIN pg_class c ON t.tgrelid = c.oid
109 WHERE c.relnamespace = 'public'::regnamespace
110 AND NOT t.tgisinternal
111 AND ($1::text IS NULL OR t.tgname ILIKE '%' || $1 || '%')
112 ORDER BY t.tgname, c.relname
113 LIMIT $2 OFFSET $3";
114
115const DETAILED_SQL: &str = r"
130 SELECT
131 t.tgname AS name,
132 json_build_object(
133 'schema', 'public',
134 'table', c.relname,
135 'status', CASE t.tgenabled
136 WHEN 'O' THEN 'ENABLED'
137 WHEN 'D' THEN 'DISABLED'
138 WHEN 'R' THEN 'REPLICA'
139 WHEN 'A' THEN 'ALWAYS'
140 END,
141 'timing', CASE
142 WHEN (t.tgtype & 2) = 2 THEN 'BEFORE'
143 WHEN (t.tgtype & 64) = 64 THEN 'INSTEAD OF'
144 ELSE 'AFTER'
145 END,
146 'events', to_json(array_remove(ARRAY[
147 CASE WHEN (t.tgtype & 4) = 4 THEN 'INSERT' END,
148 CASE WHEN (t.tgtype & 16) = 16 THEN 'UPDATE' END,
149 CASE WHEN (t.tgtype & 8) = 8 THEN 'DELETE' END,
150 CASE WHEN (t.tgtype & 32) = 32 THEN 'TRUNCATE' END
151 ], NULL)),
152 'activationLevel', CASE WHEN (t.tgtype & 1) = 1 THEN 'ROW' ELSE 'STATEMENT' END,
153 'functionName', p.proname,
154 'definition', pg_get_triggerdef(t.oid)
155 ) AS entry
156 FROM pg_trigger t
157 JOIN pg_class c ON t.tgrelid = c.oid
158 LEFT JOIN pg_proc p ON p.oid = t.tgfoid
159 WHERE c.relnamespace = 'public'::regnamespace
160 AND NOT t.tgisinternal
161 AND ($1::text IS NULL OR t.tgname ILIKE '%' || $1 || '%')
162 ORDER BY t.tgname, c.relname
163 LIMIT $2 OFFSET $3";
164
165impl PostgresHandler {
166 pub async fn list_triggers(
174 &self,
175 database: Option<String>,
176 cursor: Option<Cursor>,
177 search: Option<String>,
178 detailed: bool,
179 ) -> Result<ListTriggersResponse, ErrorData> {
180 let database = database.as_deref().map(str::trim).filter(|s| !s.is_empty());
181 let pattern = search.as_deref().map(str::trim).filter(|s| !s.is_empty());
182 let pager = Pager::new(cursor, self.config.page_size);
183
184 if detailed {
185 let rows: Vec<(String, sqlx::types::Json<serde_json::Value>)> = self
186 .connection
187 .fetch(
188 sqlx::query(DETAILED_SQL)
189 .bind(pattern)
190 .bind(pager.limit())
191 .bind(pager.offset()),
192 database,
193 )
194 .await?;
195 let (rows, next_cursor) = pager.paginate(rows);
196 return Ok(ListTriggersResponse::detailed(
197 rows.into_iter().map(|(name, json)| (name, json.0)).collect(),
198 next_cursor,
199 ));
200 }
201
202 let rows: Vec<String> = self
203 .connection
204 .fetch_scalar(
205 sqlx::query(BRIEF_SQL)
206 .bind(pattern)
207 .bind(pager.limit())
208 .bind(pager.offset()),
209 database,
210 )
211 .await?;
212 let (triggers, next_cursor) = pager.paginate(rows);
213 Ok(ListTriggersResponse::brief(triggers, next_cursor))
214 }
215}