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(
179 &self,
180 database: Option<String>,
181 cursor: Option<Cursor>,
182 search: Option<String>,
183 detailed: bool,
184 ) -> Result<ListEntriesResponse, ErrorData> {
185 let database = database.as_deref().map(str::trim).filter(|s| !s.is_empty());
186 let pattern = search.as_deref().map(str::trim).filter(|s| !s.is_empty());
187 let pager = Pager::new(cursor, self.config.page_size);
188
189 if detailed {
190 let rows: Vec<(String, sqlx::types::Json<serde_json::Value>)> = self
191 .connection
192 .fetch(
193 sqlx::query(DETAILED_SQL)
194 .bind(pattern)
195 .bind(pager.limit())
196 .bind(pager.offset()),
197 database,
198 )
199 .await?;
200 let (rows, next_cursor) = pager.paginate(rows);
201 return Ok(ListEntriesResponse::detailed(
202 rows.into_iter().map(|(name, json)| (name, json.0)).collect(),
203 next_cursor,
204 ));
205 }
206
207 let rows: Vec<String> = self
208 .connection
209 .fetch_scalar(
210 sqlx::query(BRIEF_SQL)
211 .bind(pattern)
212 .bind(pager.limit())
213 .bind(pager.offset()),
214 database,
215 )
216 .await?;
217 let (triggers, next_cursor) = pager.paginate(rows);
218 Ok(ListEntriesResponse::brief(triggers, next_cursor))
219 }
220}