dbmcp_postgres/tools/
list_triggers.rs1use std::borrow::Cow;
4
5use dbmcp_server::pagination::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::{ListTriggersRequest, ListTriggersResponse};
12
13pub(crate) struct ListTriggersTool;
15
16impl ListTriggersTool {
17 const NAME: &'static str = "listTriggers";
18 const TITLE: &'static str = "List Triggers";
19 const DESCRIPTION: &'static str = include_str!("../../assets/tools/list_triggers.md");
20}
21
22impl ToolBase for ListTriggersTool {
23 type Parameter = ListTriggersRequest;
24 type Output = ListTriggersResponse;
25 type Error = ErrorData;
26
27 fn name() -> Cow<'static, str> {
28 Self::NAME.into()
29 }
30
31 fn title() -> Option<String> {
32 Some(Self::TITLE.into())
33 }
34
35 fn description() -> Option<Cow<'static, str>> {
36 Some(Self::DESCRIPTION.into())
37 }
38
39 fn annotations() -> Option<ToolAnnotations> {
40 Some(
41 ToolAnnotations::new()
42 .read_only(true)
43 .destructive(false)
44 .idempotent(true)
45 .open_world(false),
46 )
47 }
48}
49
50impl AsyncTool<PostgresHandler> for ListTriggersTool {
51 async fn invoke(handler: &PostgresHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
52 handler.list_triggers(params).await
53 }
54}
55
56const BRIEF_SQL: &str = r"
65 SELECT t.tgname
66 FROM pg_trigger t
67 JOIN pg_class c ON t.tgrelid = c.oid
68 WHERE c.relnamespace = 'public'::regnamespace
69 AND NOT t.tgisinternal
70 AND ($1::text IS NULL OR t.tgname ILIKE '%' || $1 || '%')
71 ORDER BY t.tgname, c.relname
72 LIMIT $2 OFFSET $3";
73
74const DETAILED_SQL: &str = r"
89 SELECT
90 t.tgname AS name,
91 json_build_object(
92 'schema', 'public',
93 'table', c.relname,
94 'status', CASE t.tgenabled
95 WHEN 'O' THEN 'ENABLED'
96 WHEN 'D' THEN 'DISABLED'
97 WHEN 'R' THEN 'REPLICA'
98 WHEN 'A' THEN 'ALWAYS'
99 END,
100 'timing', CASE
101 WHEN (t.tgtype & 2) = 2 THEN 'BEFORE'
102 WHEN (t.tgtype & 64) = 64 THEN 'INSTEAD OF'
103 ELSE 'AFTER'
104 END,
105 'events', to_json(array_remove(ARRAY[
106 CASE WHEN (t.tgtype & 4) = 4 THEN 'INSERT' END,
107 CASE WHEN (t.tgtype & 16) = 16 THEN 'UPDATE' END,
108 CASE WHEN (t.tgtype & 8) = 8 THEN 'DELETE' END,
109 CASE WHEN (t.tgtype & 32) = 32 THEN 'TRUNCATE' END
110 ], NULL)),
111 'activationLevel', CASE WHEN (t.tgtype & 1) = 1 THEN 'ROW' ELSE 'STATEMENT' END,
112 'functionName', p.proname,
113 'definition', pg_get_triggerdef(t.oid)
114 ) AS entry
115 FROM pg_trigger t
116 JOIN pg_class c ON t.tgrelid = c.oid
117 LEFT JOIN pg_proc p ON p.oid = t.tgfoid
118 WHERE c.relnamespace = 'public'::regnamespace
119 AND NOT t.tgisinternal
120 AND ($1::text IS NULL OR t.tgname ILIKE '%' || $1 || '%')
121 ORDER BY t.tgname, c.relname
122 LIMIT $2 OFFSET $3";
123
124impl PostgresHandler {
125 pub async fn list_triggers(
133 &self,
134 ListTriggersRequest {
135 database,
136 cursor,
137 search,
138 detailed,
139 }: ListTriggersRequest,
140 ) -> Result<ListTriggersResponse, ErrorData> {
141 let database = database.as_deref().map(str::trim).filter(|s| !s.is_empty());
142 let pattern = search.as_deref().map(str::trim).filter(|s| !s.is_empty());
143 let pager = Pager::new(cursor, self.config.page_size);
144
145 if detailed {
146 let rows: Vec<(String, sqlx::types::Json<serde_json::Value>)> = self
147 .connection
148 .fetch(
149 sqlx::query(DETAILED_SQL)
150 .bind(pattern)
151 .bind(pager.limit())
152 .bind(pager.offset()),
153 database,
154 )
155 .await?;
156 let (rows, next_cursor) = pager.paginate(rows);
157 return Ok(ListTriggersResponse::detailed(
158 rows.into_iter().map(|(name, json)| (name, json.0)).collect(),
159 next_cursor,
160 ));
161 }
162
163 let rows: Vec<String> = self
164 .connection
165 .fetch_scalar(
166 sqlx::query(BRIEF_SQL)
167 .bind(pattern)
168 .bind(pager.limit())
169 .bind(pager.offset()),
170 database,
171 )
172 .await?;
173 let (triggers, next_cursor) = pager.paginate(rows);
174 Ok(ListTriggersResponse::brief(triggers, next_cursor))
175 }
176}