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 = r#"List user-defined triggers in the `public` schema, optionally filtered and/or with full metadata.
20
21<usecase>
22Use when:
23- Auditing triggers across a database (brief mode, default).
24- Searching for a trigger by partial name (pass `search`).
25- Inspecting a trigger's timing, events, activation level, handler function, status, and full `CREATE TRIGGER` text before reasoning about side-effects (pass `detailed: true`). Detailed mode supersedes ad-hoc `readQuery` against `pg_trigger` / `information_schema.triggers`.
26</usecase>
27
28<parameters>
29- `database` — Database to target. Defaults to the active database.
30- `cursor` — Opaque pagination cursor; echo the prior response's `nextCursor`.
31- `search` — Case-insensitive filter on trigger names via `ILIKE`. `%` matches any sequence; `_` matches a single character.
32- `detailed` — When `true`, returns full metadata objects keyed by trigger name instead of bare name strings. Default `false`.
33</parameters>
34
35<examples>
36✓ "What triggers are in the mydb database?" → listTriggers(database="mydb")
37✓ "Find the audit triggers" → listTriggers(search="audit")
38✓ "What does orders_audit_after_iu do?" → listTriggers(search="orders_audit_after_iu", detailed=true)
39✗ "Show me a trigger's body" → use detailed mode; the `definition` field carries the full `CREATE TRIGGER` text
40</examples>
41
42<what_it_returns>
43Brief mode (default): a sorted JSON array of trigger-name strings, e.g. `["customers_audit_after_insert", "orders_audit_after_insert"]`.
44Detailed mode: a JSON object keyed by trigger name; each value carries `schema`, `table`, `status` (ENABLED/DISABLED/REPLICA/ALWAYS), `timing` (BEFORE/AFTER/INSTEAD OF), `events` (array of strings drawn from INSERT/UPDATE/DELETE/TRUNCATE in that fixed order), `activationLevel` (ROW/STATEMENT), `functionName`, and `definition` (the full `CREATE TRIGGER` text). Internal triggers (FK enforcement etc.) are excluded.
45</what_it_returns>
46
47<pagination>
48Paginated. Pass the prior response's `nextCursor` as `cursor` to fetch the next page. The `search` filter must stay the same across pages for cursor continuity.
49</pagination>"#;
50}
51
52impl ToolBase for ListTriggersTool {
53 type Parameter = ListTriggersRequest;
54 type Output = ListTriggersResponse;
55 type Error = ErrorData;
56
57 fn name() -> Cow<'static, str> {
58 Self::NAME.into()
59 }
60
61 fn title() -> Option<String> {
62 Some(Self::TITLE.into())
63 }
64
65 fn description() -> Option<Cow<'static, str>> {
66 Some(Self::DESCRIPTION.into())
67 }
68
69 fn annotations() -> Option<ToolAnnotations> {
70 Some(
71 ToolAnnotations::new()
72 .read_only(true)
73 .destructive(false)
74 .idempotent(true)
75 .open_world(false),
76 )
77 }
78}
79
80impl AsyncTool<PostgresHandler> for ListTriggersTool {
81 async fn invoke(handler: &PostgresHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
82 handler.list_triggers(params).await
83 }
84}
85
86const BRIEF_SQL: &str = r"
95 SELECT t.tgname
96 FROM pg_trigger t
97 JOIN pg_class c ON t.tgrelid = c.oid
98 WHERE c.relnamespace = 'public'::regnamespace
99 AND NOT t.tgisinternal
100 AND ($1::text IS NULL OR t.tgname ILIKE '%' || $1 || '%')
101 ORDER BY t.tgname, c.relname
102 LIMIT $2 OFFSET $3";
103
104const DETAILED_SQL: &str = r"
119 SELECT
120 t.tgname AS name,
121 json_build_object(
122 'schema', 'public',
123 'table', c.relname,
124 'status', CASE t.tgenabled
125 WHEN 'O' THEN 'ENABLED'
126 WHEN 'D' THEN 'DISABLED'
127 WHEN 'R' THEN 'REPLICA'
128 WHEN 'A' THEN 'ALWAYS'
129 END,
130 'timing', CASE
131 WHEN (t.tgtype & 2) = 2 THEN 'BEFORE'
132 WHEN (t.tgtype & 64) = 64 THEN 'INSTEAD OF'
133 ELSE 'AFTER'
134 END,
135 'events', to_json(array_remove(ARRAY[
136 CASE WHEN (t.tgtype & 4) = 4 THEN 'INSERT' END,
137 CASE WHEN (t.tgtype & 16) = 16 THEN 'UPDATE' END,
138 CASE WHEN (t.tgtype & 8) = 8 THEN 'DELETE' END,
139 CASE WHEN (t.tgtype & 32) = 32 THEN 'TRUNCATE' END
140 ], NULL)),
141 'activationLevel', CASE WHEN (t.tgtype & 1) = 1 THEN 'ROW' ELSE 'STATEMENT' END,
142 'functionName', p.proname,
143 'definition', pg_get_triggerdef(t.oid)
144 ) AS entry
145 FROM pg_trigger t
146 JOIN pg_class c ON t.tgrelid = c.oid
147 LEFT JOIN pg_proc p ON p.oid = t.tgfoid
148 WHERE c.relnamespace = 'public'::regnamespace
149 AND NOT t.tgisinternal
150 AND ($1::text IS NULL OR t.tgname ILIKE '%' || $1 || '%')
151 ORDER BY t.tgname, c.relname
152 LIMIT $2 OFFSET $3";
153
154impl PostgresHandler {
155 pub async fn list_triggers(
163 &self,
164 ListTriggersRequest {
165 database,
166 cursor,
167 search,
168 detailed,
169 }: ListTriggersRequest,
170 ) -> Result<ListTriggersResponse, ErrorData> {
171 let database = database.as_deref().map(str::trim).filter(|s| !s.is_empty());
172 let pattern = search.as_deref().map(str::trim).filter(|s| !s.is_empty());
173 let pager = Pager::new(cursor, self.config.page_size);
174
175 if detailed {
176 let rows: Vec<(String, sqlx::types::Json<serde_json::Value>)> = self
177 .connection
178 .fetch(
179 sqlx::query(DETAILED_SQL)
180 .bind(pattern)
181 .bind(pager.limit())
182 .bind(pager.offset()),
183 database,
184 )
185 .await?;
186 let (rows, next_cursor) = pager.paginate(rows);
187 return Ok(ListTriggersResponse::detailed(
188 rows.into_iter().map(|(name, json)| (name, json.0)).collect(),
189 next_cursor,
190 ));
191 }
192
193 let rows: Vec<String> = self
194 .connection
195 .fetch_scalar(
196 sqlx::query(BRIEF_SQL)
197 .bind(pattern)
198 .bind(pager.limit())
199 .bind(pager.offset()),
200 database,
201 )
202 .await?;
203 let (triggers, next_cursor) = pager.paginate(rows);
204 Ok(ListTriggersResponse::brief(triggers, next_cursor))
205 }
206}