mixtape_tools/sqlite/maintenance/
export_schema.rs1use crate::prelude::*;
4use crate::sqlite::manager::with_connection;
5use crate::sqlite::types::{ColumnDefinition, SchemaFormat, TableInfo};
6
7#[derive(Debug, Deserialize, JsonSchema)]
9pub struct ExportSchemaInput {
10 #[serde(default)]
12 pub db_path: Option<String>,
13
14 #[serde(default)]
16 pub format: SchemaFormat,
17
18 #[serde(default)]
20 pub tables: Vec<String>,
21}
22
23pub struct ExportSchemaTool;
28
29impl Tool for ExportSchemaTool {
30 type Input = ExportSchemaInput;
31
32 fn name(&self) -> &str {
33 "sqlite_export_schema"
34 }
35
36 fn description(&self) -> &str {
37 "Export the database schema in SQL or JSON format. Can export all tables or specific tables."
38 }
39
40 async fn execute(&self, input: Self::Input) -> Result<ToolResult, ToolError> {
41 let format = input.format;
42 let filter_tables = input.tables;
43
44 let result = with_connection(input.db_path, move |conn| {
45 let mut stmt = conn.prepare(
47 "SELECT name, type, sql FROM sqlite_master
48 WHERE type IN ('table', 'view')
49 AND name NOT LIKE 'sqlite_%'
50 ORDER BY type, name",
51 )?;
52
53 let objects: Vec<(String, String, Option<String>)> = stmt
54 .query_map([], |row| {
55 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
56 })?
57 .filter_map(|r| r.ok())
58 .filter(|(name, _, _)| {
59 filter_tables.is_empty() || filter_tables.contains(name)
60 })
61 .collect();
62
63 match format {
64 SchemaFormat::Sql => {
65 let mut sql = String::new();
67 for (name, obj_type, create_sql) in &objects {
68 if let Some(s) = create_sql {
69 sql.push_str(&format!("-- {} '{}'\n", obj_type, name));
70 sql.push_str(s);
71 sql.push_str(";\n\n");
72 }
73 }
74
75 let mut idx_stmt = conn.prepare(
77 "SELECT name, sql FROM sqlite_master WHERE type = 'index' AND sql IS NOT NULL"
78 )?;
79 let indexes: Vec<(String, String)> = idx_stmt
80 .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
81 .filter_map(|r| r.ok())
82 .collect();
83
84 if !indexes.is_empty() {
85 sql.push_str("-- Indexes\n");
86 for (name, create_sql) in indexes {
87 sql.push_str(&format!("-- index '{}'\n", name));
88 sql.push_str(&create_sql);
89 sql.push_str(";\n\n");
90 }
91 }
92
93 Ok(serde_json::json!({
94 "format": "sql",
95 "schema": sql,
96 "table_count": objects.len()
97 }))
98 }
99 SchemaFormat::Json => {
100 let mut tables = Vec::new();
102
103 for (name, table_type, _) in &objects {
104 let mut col_stmt =
106 conn.prepare(&format!("PRAGMA table_info('{}')", name))?;
107
108 let columns: Vec<ColumnDefinition> = col_stmt
109 .query_map([], |row| {
110 let pk: i32 = row.get(5)?;
111 let notnull: i32 = row.get(3)?;
112 Ok(ColumnDefinition {
113 name: row.get(1)?,
114 data_type: row.get(2)?,
115 nullable: notnull == 0,
116 primary_key: pk > 0,
117 default: row.get(4)?,
118 })
119 })?
120 .filter_map(|r| r.ok())
121 .collect();
122
123 tables.push(TableInfo {
124 name: name.clone(),
125 table_type: table_type.clone(),
126 columns,
127 row_count: None,
128 });
129 }
130
131 Ok(serde_json::json!({
132 "format": "json",
133 "tables": tables,
134 "table_count": tables.len()
135 }))
136 }
137 }
138 })
139 .await?;
140
141 Ok(ToolResult::Json(result))
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::sqlite::test_utils::{unwrap_json, TestDatabase};
149
150 #[tokio::test]
151 async fn test_export_schema_sql() {
152 let db = TestDatabase::with_schema(
153 "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
154 CREATE TABLE posts (id INTEGER, user_id INTEGER);",
155 )
156 .await;
157
158 let tool = ExportSchemaTool;
159 let input = ExportSchemaInput {
160 db_path: Some(db.key()),
161 format: SchemaFormat::Sql,
162 tables: vec![],
163 };
164
165 let result = tool.execute(input).await.unwrap();
166 let json = unwrap_json(result);
167
168 assert_eq!(json["format"].as_str().unwrap(), "sql");
169 assert_eq!(json["table_count"].as_i64().unwrap(), 2);
170 let schema = json["schema"].as_str().unwrap();
171 assert!(schema.contains("CREATE TABLE"));
172 }
173
174 #[tokio::test]
175 async fn test_export_schema_json() {
176 let db =
177 TestDatabase::with_schema("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);")
178 .await;
179
180 let tool = ExportSchemaTool;
181 let input = ExportSchemaInput {
182 db_path: Some(db.key()),
183 format: SchemaFormat::Json,
184 tables: vec![],
185 };
186
187 let result = tool.execute(input).await.unwrap();
188 let json = unwrap_json(result);
189
190 assert_eq!(json["format"].as_str().unwrap(), "json");
191 let tables = json["tables"].as_array().unwrap();
192 assert_eq!(tables.len(), 1);
193 assert_eq!(tables[0]["name"].as_str().unwrap(), "users");
194 }
195
196 #[test]
197 fn test_tool_metadata() {
198 let tool = ExportSchemaTool;
199 assert_eq!(tool.name(), "sqlite_export_schema");
200 assert!(!tool.description().is_empty());
201 }
202}