Skip to main content

mixtape_tools/sqlite/table/
list.rs

1//! List tables tool
2
3use crate::prelude::*;
4use crate::sqlite::manager::with_connection;
5
6/// Input for listing tables
7#[derive(Debug, Deserialize, JsonSchema)]
8pub struct ListTablesInput {
9    /// Database file path. If not specified, uses the default database.
10    #[serde(default)]
11    pub db_path: Option<String>,
12}
13
14/// Table entry information
15#[derive(Debug, Serialize, JsonSchema)]
16struct TableEntry {
17    name: String,
18    #[serde(rename = "type")]
19    table_type: String,
20}
21
22/// Tool for listing all tables and views in a database
23///
24/// Returns a list of all tables and views, excluding:
25/// - SQLite internal tables (`sqlite_*`)
26/// - System tables managed by tools (`_*`)
27pub struct ListTablesTool;
28
29impl Tool for ListTablesTool {
30    type Input = ListTablesInput;
31
32    fn name(&self) -> &str {
33        "sqlite_list_tables"
34    }
35
36    fn description(&self) -> &str {
37        "List all tables and views in a SQLite database. Excludes SQLite internal tables (sqlite_*) \
38         and system tables managed by tools (_*). Returns the name and type of each table/view."
39    }
40
41    async fn execute(&self, input: Self::Input) -> Result<ToolResult, ToolError> {
42        let tables = with_connection(input.db_path, |conn| {
43            let mut stmt = conn.prepare(
44                "SELECT name, type FROM sqlite_master
45                 WHERE type IN ('table', 'view')
46                 AND name NOT LIKE 'sqlite_%'
47                 AND name NOT LIKE '\\_%' ESCAPE '\\'
48                 ORDER BY type, name",
49            )?;
50
51            let tables: Vec<TableEntry> = stmt
52                .query_map([], |row| {
53                    Ok(TableEntry {
54                        name: row.get(0)?,
55                        table_type: row.get(1)?,
56                    })
57                })?
58                .filter_map(|r| r.ok())
59                .collect();
60
61            Ok(tables)
62        })
63        .await?;
64
65        let count = tables.len();
66        Ok(ToolResult::Json(serde_json::json!({
67            "tables": tables,
68            "count": count
69        })))
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::sqlite::test_utils::{unwrap_json, TestDatabase};
77
78    #[tokio::test]
79    async fn test_list_tables() {
80        let db = TestDatabase::with_schema(
81            "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
82             CREATE TABLE posts (id INTEGER PRIMARY KEY, user_id INTEGER);
83             CREATE VIEW user_posts AS SELECT * FROM users JOIN posts ON users.id = posts.user_id;",
84        )
85        .await;
86
87        let result = ListTablesTool
88            .execute(ListTablesInput {
89                db_path: Some(db.key()),
90            })
91            .await
92            .unwrap();
93
94        let json = unwrap_json(result);
95        assert_eq!(json["count"].as_i64().unwrap(), 3);
96    }
97
98    #[test]
99    fn test_tool_metadata() {
100        let tool = ListTablesTool;
101        assert_eq!(tool.name(), "sqlite_list_tables");
102        assert!(!tool.description().is_empty());
103    }
104
105    #[tokio::test]
106    async fn test_excludes_system_tables() {
107        let db = TestDatabase::with_schema(
108            "CREATE TABLE users (id INTEGER PRIMARY KEY);
109             CREATE TABLE _system_table (id INTEGER PRIMARY KEY);",
110        )
111        .await;
112
113        let result = ListTablesTool
114            .execute(ListTablesInput {
115                db_path: Some(db.key()),
116            })
117            .await
118            .unwrap();
119
120        let json = unwrap_json(result);
121        // Should only show 'users', not '_system_table'
122        assert_eq!(json["count"].as_i64().unwrap(), 1);
123        assert_eq!(json["tables"][0]["name"], "users");
124    }
125}