mixtape_tools/sqlite/query/
schema.rs1use crate::prelude::*;
4use crate::sqlite::error::SqliteToolError;
5use crate::sqlite::manager::with_connection;
6use crate::sqlite::types::json_to_sql;
7
8#[derive(Debug, Deserialize, JsonSchema)]
10pub struct SchemaQueryInput {
11 pub query: String,
13
14 #[serde(default)]
16 pub params: Vec<serde_json::Value>,
17
18 #[serde(default)]
20 pub db_path: Option<String>,
21}
22
23pub struct SchemaQueryTool;
28
29impl SchemaQueryTool {
30 fn is_schema_query(sql: &str) -> bool {
32 let normalized = sql.trim().to_uppercase();
33 let ddl_prefixes = ["CREATE", "ALTER", "DROP"];
34 ddl_prefixes
35 .iter()
36 .any(|prefix| normalized.starts_with(prefix))
37 }
38}
39
40impl Tool for SchemaQueryTool {
41 type Input = SchemaQueryInput;
42
43 fn name(&self) -> &str {
44 "sqlite_schema_query"
45 }
46
47 fn description(&self) -> &str {
48 "Execute a DDL (Data Definition Language) SQL query (CREATE, ALTER, DROP). Use for schema modifications."
49 }
50
51 async fn execute(&self, input: Self::Input) -> Result<ToolResult, ToolError> {
52 if !Self::is_schema_query(&input.query) {
54 return Err(SqliteToolError::InvalidQuery(
55 "Only CREATE, ALTER, and DROP queries are allowed. Use sqlite_write_query for INSERT/UPDATE/DELETE.".to_string()
56 ).into());
57 }
58
59 let query = input.query.clone();
60 let params = input.params;
61
62 with_connection(input.db_path, move |conn| {
63 let params_ref: Vec<Box<dyn rusqlite::ToSql>> =
65 params.iter().map(|v| json_to_sql(v)).collect();
66
67 let params_slice: Vec<&dyn rusqlite::ToSql> =
68 params_ref.iter().map(|b| b.as_ref()).collect();
69
70 conn.execute(&query, params_slice.as_slice())?;
71
72 Ok(())
73 })
74 .await?;
75
76 let response = serde_json::json!({
77 "status": "success",
78 "message": format!("Schema query executed successfully")
79 });
80 Ok(ToolResult::Json(response))
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::sqlite::test_utils::TestDatabase;
88
89 #[tokio::test]
90 async fn test_schema_query_create() {
91 let db = TestDatabase::new().await;
92
93 let tool = SchemaQueryTool;
94 let input = SchemaQueryInput {
95 query: "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)".to_string(),
96 params: vec![],
97 db_path: Some(db.key()),
98 };
99
100 let result = tool.execute(input).await;
101 assert!(result.is_ok());
102
103 let rows =
105 db.query("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='users'");
106 assert_eq!(rows[0][0], 1);
107 }
108
109 #[tokio::test]
110 async fn test_schema_query_alter() {
111 let db = TestDatabase::with_schema("CREATE TABLE users (id INTEGER);").await;
112
113 let tool = SchemaQueryTool;
114 let input = SchemaQueryInput {
115 query: "ALTER TABLE users ADD COLUMN name TEXT".to_string(),
116 params: vec![],
117 db_path: Some(db.key()),
118 };
119
120 let result = tool.execute(input).await;
121 assert!(result.is_ok());
122 }
123
124 #[tokio::test]
125 async fn test_reject_select() {
126 let db = TestDatabase::new().await;
127
128 let tool = SchemaQueryTool;
129 let input = SchemaQueryInput {
130 query: "SELECT * FROM users".to_string(),
131 params: vec![],
132 db_path: Some(db.key()),
133 };
134
135 let result = tool.execute(input).await;
136 assert!(result.is_err());
137 }
138
139 #[test]
140 fn test_is_schema_query() {
141 assert!(SchemaQueryTool::is_schema_query(
142 "CREATE TABLE users (id INT)"
143 ));
144 assert!(SchemaQueryTool::is_schema_query(
145 "ALTER TABLE users ADD COLUMN name TEXT"
146 ));
147 assert!(SchemaQueryTool::is_schema_query("DROP TABLE users"));
148 assert!(SchemaQueryTool::is_schema_query(
149 "CREATE INDEX idx ON users(id)"
150 ));
151
152 assert!(!SchemaQueryTool::is_schema_query("SELECT * FROM users"));
153 assert!(!SchemaQueryTool::is_schema_query(
154 "INSERT INTO users VALUES (1)"
155 ));
156 assert!(!SchemaQueryTool::is_schema_query(
157 "UPDATE users SET name = 'x'"
158 ));
159 }
160
161 #[test]
162 fn test_tool_metadata() {
163 let tool = SchemaQueryTool;
164 assert_eq!(tool.name(), "sqlite_schema_query");
165 assert!(!tool.description().is_empty());
166 }
167}