1use dbmcp_server::pagination::Pager;
4
5use super::prelude::*;
6use crate::types::{ListEntriesResponse, ListTablesRequest};
7
8const NAME: &str = "listTables";
9const TITLE: &str = "List Tables";
10const DESCRIPTION: &str = include_str!("../../assets/tools/list_tables.md");
11
12pub(crate) struct ListTablesTool;
14
15impl ToolBase for ListTablesTool {
16 type Parameter = ListTablesRequest;
17 type Output = ListEntriesResponse;
18 type Error = ErrorData;
19
20 fn name() -> Cow<'static, str> {
21 NAME.into()
22 }
23
24 fn title() -> Option<String> {
25 Some(TITLE.into())
26 }
27
28 fn description() -> Option<Cow<'static, str>> {
29 Some(DESCRIPTION.into())
30 }
31
32 fn annotations() -> Option<ToolAnnotations> {
33 Some(
34 ToolAnnotations::new()
35 .read_only(true)
36 .destructive(false)
37 .idempotent(true)
38 .open_world(false),
39 )
40 }
41
42 fn input_schema() -> Option<Arc<JsonObject>> {
43 Some(input_schema::<Self::Parameter>(true))
44 }
45
46 fn output_schema() -> Option<Arc<JsonObject>> {
47 Some(output_schema::<Self::Output>())
48 }
49}
50
51impl AsyncTool<SqliteHandler> for ListTablesTool {
52 async fn invoke(handler: &SqliteHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
53 handler.list_tables(params).await
54 }
55}
56
57const BRIEF_SQL: &str = r"
73 SELECT tl.name
74 FROM pragma_table_list tl
75 WHERE tl.schema = 'main'
76 AND tl.type IN ('table', 'virtual')
77 AND tl.name NOT LIKE 'sqlite_%'
78 AND (?1 IS NULL OR tl.name LIKE '%' || ?1 || '%')
79 ORDER BY tl.name
80 LIMIT ?2 OFFSET ?3";
81
82const DETAILED_SQL: &str = r#"
99 WITH table_info AS (
100 SELECT
101 tl.name AS table_name,
102 CASE tl.type
103 WHEN 'virtual' THEN 'VIRTUAL_TABLE'
104 ELSE 'TABLE'
105 END AS kind
106 FROM pragma_table_list tl
107 WHERE tl.schema = 'main'
108 AND tl.type IN ('table', 'virtual')
109 AND tl.name NOT LIKE 'sqlite_%'
110 AND (?1 IS NULL OR tl.name LIKE '%' || ?1 || '%')
111 ORDER BY tl.name
112 LIMIT ?2 OFFSET ?3
113 ),
114 columns_info AS (
115 SELECT
116 ti.table_name,
117 c.cid AS cid,
118 json_object(
119 'name', c.name,
120 'dataType', c.type,
121 'ordinalPosition', c.cid + 1,
122 'nullable', json(CASE WHEN c."notnull" = 0 THEN 'true' ELSE 'false' END),
123 'default', c.dflt_value,
124 'comment', NULL
125 ) AS column_json
126 FROM table_info ti, pragma_table_info(ti.table_name) c
127 ),
128 pk_constraints AS (
129 SELECT
130 ti.table_name,
131 json_object(
132 'name', 'PRIMARY',
133 'type', 'PRIMARY KEY',
134 'columns', json_group_array(c.name),
135 'definition', 'PRIMARY KEY (' || group_concat('"' || c.name || '"', ', ') || ')'
136 ) AS constraint_json
137 FROM table_info ti, pragma_table_info(ti.table_name) c
138 WHERE c.pk > 0
139 GROUP BY ti.table_name
140 HAVING COUNT(c.name) > 0
141 ),
142 fk_constraints AS (
143 SELECT
144 ti.table_name,
145 json_object(
146 'name', 'fk_' || ti.table_name || '_' || f.id,
147 'type', 'FOREIGN KEY',
148 'columns', json_group_array(f."from"),
149 'definition', 'FOREIGN KEY (' || group_concat('"' || f."from" || '"', ', ')
150 || ') REFERENCES "' || f."table" || '"('
151 || group_concat('"' || f."to" || '"', ', ') || ')',
152 'referencedTable', f."table",
153 'referencedColumns', json_group_array(f."to")
154 ) AS constraint_json
155 FROM table_info ti, pragma_foreign_key_list(ti.table_name) f
156 GROUP BY ti.table_name, f.id
157 ),
158 unique_constraints AS (
159 SELECT
160 ti.table_name,
161 json_object(
162 'name', i.name,
163 'type', 'UNIQUE',
164 'columns', (SELECT json_group_array(ii.name)
165 FROM pragma_index_info(i.name) ii),
166 'definition', 'UNIQUE ('
167 || (SELECT group_concat('"' || ii.name || '"', ', ')
168 FROM pragma_index_info(i.name) ii)
169 || ')'
170 ) AS constraint_json
171 FROM table_info ti, pragma_index_list(ti.table_name) i
172 WHERE i."unique" = 1 AND i.origin <> 'pk'
173 ),
174 all_constraints AS (
175 SELECT table_name, constraint_json FROM pk_constraints
176 UNION ALL
177 SELECT table_name, constraint_json FROM fk_constraints
178 UNION ALL
179 SELECT table_name, constraint_json FROM unique_constraints
180 ),
181 indexes_info AS (
182 SELECT
183 ti.table_name,
184 json_object(
185 'name', il.name,
186 'columns', (SELECT json_group_array(ii.name) FROM pragma_index_info(il.name) ii),
187 'unique', json(CASE WHEN il."unique" = 1 THEN 'true' ELSE 'false' END),
188 'primary', json(CASE WHEN il.origin = 'pk' THEN 'true' ELSE 'false' END),
189 'method', 'btree',
190 'definition', COALESCE(
191 (SELECT m2.sql FROM sqlite_master m2 WHERE m2.type = 'index' AND m2.name = il.name),
192 'CREATE ' || CASE il."unique" WHEN 1 THEN 'UNIQUE INDEX ' ELSE 'INDEX ' END
193 || '"' || il.name || '" ON "' || ti.table_name || '"('
194 || (SELECT group_concat('"' || ii.name || '"', ', ') FROM pragma_index_info(il.name) ii)
195 || ')'
196 )
197 ) AS index_json
198 FROM table_info ti, pragma_index_list(ti.table_name) il
199 ),
200 triggers_info AS (
201 SELECT
202 m.tbl_name AS table_name,
203 json_object(
204 'name', m.name,
205 'definition', m.sql,
206 'enabled', json('true')
207 ) AS trigger_json
208 FROM sqlite_master m
209 JOIN table_info ti ON ti.table_name = m.tbl_name
210 WHERE m.type = 'trigger'
211 )
212 SELECT
213 ti.table_name AS name,
214 json_object(
215 'schema', 'main',
216 'kind', ti.kind,
217 'owner', NULL,
218 'comment', NULL,
219 'columns', COALESCE(
220 (SELECT json_group_array(json(ci.column_json))
221 FROM (SELECT column_json
222 FROM columns_info
223 WHERE table_name = ti.table_name
224 ORDER BY cid) ci),
225 json('[]')),
226 'constraints', COALESCE(
227 (SELECT json_group_array(json(ac.constraint_json))
228 FROM all_constraints ac
229 WHERE ac.table_name = ti.table_name),
230 json('[]')),
231 'indexes', COALESCE(
232 (SELECT json_group_array(json(ii.index_json))
233 FROM indexes_info ii
234 WHERE ii.table_name = ti.table_name),
235 json('[]')),
236 'triggers', COALESCE(
237 (SELECT json_group_array(json(tg.trigger_json))
238 FROM triggers_info tg
239 WHERE tg.table_name = ti.table_name),
240 json('[]'))
241 ) AS entry
242 FROM table_info ti
243 ORDER BY ti.table_name"#;
244
245impl SqliteHandler {
246 pub async fn list_tables(
253 &self,
254 ListTablesRequest {
255 cursor,
256 search,
257 detailed,
258 }: ListTablesRequest,
259 ) -> Result<ListEntriesResponse, ErrorData> {
260 let pattern = search.as_deref().map(str::trim).filter(|s| !s.is_empty());
261 let pager = Pager::new(cursor, self.config.page_size);
262
263 if detailed {
264 let rows: Vec<(String, sqlx::types::Json<serde_json::Value>)> = self
265 .connection
266 .fetch(
267 sqlx::query(DETAILED_SQL)
268 .bind(pattern)
269 .bind(pager.limit())
270 .bind(pager.offset()),
271 None,
272 )
273 .await?;
274 let (rows, next_cursor) = pager.paginate(rows);
275 return Ok(ListEntriesResponse::detailed(
276 rows.into_iter().map(|(name, json)| (name, json.0)).collect(),
277 next_cursor,
278 ));
279 }
280
281 let rows: Vec<String> = self
282 .connection
283 .fetch_scalar(
284 sqlx::query(BRIEF_SQL)
285 .bind(pattern)
286 .bind(pager.limit())
287 .bind(pager.offset()),
288 None,
289 )
290 .await?;
291 let (tables, next_cursor) = pager.paginate(rows);
292 Ok(ListEntriesResponse::brief(tables, next_cursor))
293 }
294}