Skip to main content

mixtape_tools/sqlite/query/
schema.rs

1//! Schema query tool
2
3use crate::prelude::*;
4use crate::sqlite::error::SqliteToolError;
5use crate::sqlite::manager::with_connection;
6use crate::sqlite::types::json_to_sql;
7
8/// Input for schema query execution
9#[derive(Debug, Deserialize, JsonSchema)]
10pub struct SchemaQueryInput {
11    /// SQL DDL query to execute (CREATE, ALTER, DROP)
12    pub query: String,
13
14    /// Query parameters for prepared statements
15    #[serde(default)]
16    pub params: Vec<serde_json::Value>,
17
18    /// Database file path. If not specified, uses the default database.
19    #[serde(default)]
20    pub db_path: Option<String>,
21}
22
23/// Tool for executing DDL (schema) queries (DESTRUCTIVE)
24///
25/// Executes CREATE, ALTER, and DROP statements.
26/// Use this for schema modifications rather than data modifications.
27pub struct SchemaQueryTool;
28
29impl SchemaQueryTool {
30    /// Validates that a query is a DDL operation
31    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        // Validate query is a DDL operation
53        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            // Convert params to rusqlite values
64            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        // Verify table was created
104        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}