dbmcp_postgres/tools/
list_triggers.rs1use dbmcp_server::pagination::{Cursor, Pager};
4
5use super::prelude::*;
6use crate::types::{ListEntriesResponse, ListTriggersRequest};
7
8const NAME: &str = "listTriggers";
9const TITLE: &str = "List Triggers";
10const DESCRIPTION_PINNED: &str = include_str!("../../assets/tools/list_triggers/pinned.md");
11const DESCRIPTION_UNPINNED: &str = include_str!("../../assets/tools/list_triggers/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 PinnedListTriggersTool;
23
24impl ToolBase for PinnedListTriggersTool {
25 type Parameter = ListTriggersRequest;
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 PinnedListTriggersTool {
55 async fn invoke(handler: &PostgresHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
56 handler
57 .list_triggers(None, params.cursor, params.search, params.detailed)
58 .await
59 }
60}
61
62pub(crate) struct UnpinnedListTriggersTool;
64
65impl ToolBase for UnpinnedListTriggersTool {
66 type Parameter = ListTriggersRequest;
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 UnpinnedListTriggersTool {
96 async fn invoke(handler: &PostgresHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
97 handler
98 .list_triggers(params.database, params.cursor, params.search, params.detailed)
99 .await
100 }
101}
102
103const BRIEF_SQL: &str = r"
112 SELECT t.tgname
113 FROM pg_trigger t
114 JOIN pg_class c ON t.tgrelid = c.oid
115 WHERE c.relnamespace = 'public'::regnamespace
116 AND NOT t.tgisinternal
117 AND ($1::text IS NULL OR t.tgname ILIKE '%' || $1 || '%')
118 ORDER BY t.tgname, c.relname
119 LIMIT $2 OFFSET $3";
120
121const DETAILED_SQL: &str = r"
136 SELECT
137 t.tgname AS name,
138 json_build_object(
139 'schema', 'public',
140 'table', c.relname,
141 'status', CASE t.tgenabled
142 WHEN 'O' THEN 'ENABLED'
143 WHEN 'D' THEN 'DISABLED'
144 WHEN 'R' THEN 'REPLICA'
145 WHEN 'A' THEN 'ALWAYS'
146 END,
147 'timing', CASE
148 WHEN (t.tgtype & 2) = 2 THEN 'BEFORE'
149 WHEN (t.tgtype & 64) = 64 THEN 'INSTEAD OF'
150 ELSE 'AFTER'
151 END,
152 'events', to_json(array_remove(ARRAY[
153 CASE WHEN (t.tgtype & 4) = 4 THEN 'INSERT' END,
154 CASE WHEN (t.tgtype & 16) = 16 THEN 'UPDATE' END,
155 CASE WHEN (t.tgtype & 8) = 8 THEN 'DELETE' END,
156 CASE WHEN (t.tgtype & 32) = 32 THEN 'TRUNCATE' END
157 ], NULL)),
158 'activationLevel', CASE WHEN (t.tgtype & 1) = 1 THEN 'ROW' ELSE 'STATEMENT' END,
159 'functionName', p.proname,
160 'definition', pg_get_triggerdef(t.oid)
161 ) AS entry
162 FROM pg_trigger t
163 JOIN pg_class c ON t.tgrelid = c.oid
164 LEFT JOIN pg_proc p ON p.oid = t.tgfoid
165 WHERE c.relnamespace = 'public'::regnamespace
166 AND NOT t.tgisinternal
167 AND ($1::text IS NULL OR t.tgname ILIKE '%' || $1 || '%')
168 ORDER BY t.tgname, c.relname
169 LIMIT $2 OFFSET $3";
170
171impl PostgresHandler {
172 pub async fn list_triggers(
180 &self,
181 database: Option<String>,
182 cursor: Option<Cursor>,
183 search: Option<String>,
184 detailed: bool,
185 ) -> Result<ListEntriesResponse, ErrorData> {
186 let database = database.as_deref().map(str::trim).filter(|s| !s.is_empty());
187 let pattern = search.as_deref().map(str::trim).filter(|s| !s.is_empty());
188 let pager = Pager::new(cursor, self.config.page_size);
189
190 if detailed {
191 let rows: Vec<(String, sqlx::types::Json<serde_json::Value>)> = self
192 .connection
193 .fetch(
194 sqlx::query(DETAILED_SQL)
195 .bind(pattern)
196 .bind(pager.limit())
197 .bind(pager.offset()),
198 database,
199 )
200 .await?;
201 let (rows, next_cursor) = pager.paginate(rows);
202 return Ok(ListEntriesResponse::detailed(
203 rows.into_iter().map(|(name, json)| (name, json.0)).collect(),
204 next_cursor,
205 ));
206 }
207
208 let rows: Vec<String> = self
209 .connection
210 .fetch_scalar(
211 sqlx::query(BRIEF_SQL)
212 .bind(pattern)
213 .bind(pager.limit())
214 .bind(pager.offset()),
215 database,
216 )
217 .await?;
218 let (triggers, next_cursor) = pager.paginate(rows);
219 Ok(ListEntriesResponse::brief(triggers, next_cursor))
220 }
221}