oxify_mcp/servers/
database.rs

1//! Database MCP server - provides SQL database operations
2//!
3//! This module implements a Model Context Protocol server for database operations,
4//! supporting SQLite queries, commands, and transactions.
5//!
6//! # Features
7//!
8//! Enable the `database` feature to use sqlx-backed database operations:
9//!
10//! ```toml
11//! oxify-mcp = { version = "0.1", features = ["database"] }
12//! ```
13
14use crate::{McpServer, Result};
15use async_trait::async_trait;
16use serde::{Deserialize, Serialize};
17use serde_json::{json, Value};
18
19#[cfg(feature = "database")]
20use sqlx::{sqlite::SqlitePool, Column, Row, TypeInfo};
21
22/// Database type enumeration
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25#[derive(Default)]
26pub enum DatabaseType {
27    /// PostgreSQL database (not supported)
28    Postgres,
29    /// MySQL database (not supported)
30    Mysql,
31    /// SQLite database
32    #[default]
33    Sqlite,
34}
35
36/// Configuration for the database server
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct DatabaseConfig {
39    /// Database connection string
40    pub connection_string: String,
41    /// Database type
42    pub db_type: DatabaseType,
43    /// Maximum number of connections in the pool
44    #[serde(default = "default_max_connections")]
45    pub max_connections: u32,
46    /// Connection timeout in seconds
47    #[serde(default = "default_timeout")]
48    pub timeout_secs: u64,
49    /// Whether to enable read-only mode (disables mutations)
50    #[serde(default)]
51    pub read_only: bool,
52    /// Maximum rows to return from queries
53    #[serde(default = "default_max_rows")]
54    pub max_rows: usize,
55}
56
57fn default_max_connections() -> u32 {
58    5
59}
60
61fn default_timeout() -> u64 {
62    30
63}
64
65fn default_max_rows() -> usize {
66    1000
67}
68
69impl DatabaseConfig {
70    /// Create a new SQLite configuration
71    pub fn sqlite(connection_string: impl Into<String>) -> Self {
72        Self {
73            connection_string: connection_string.into(),
74            db_type: DatabaseType::Sqlite,
75            max_connections: default_max_connections(),
76            timeout_secs: default_timeout(),
77            read_only: false,
78            max_rows: default_max_rows(),
79        }
80    }
81
82    /// Create a new PostgreSQL configuration (deprecated, SQLite is now default)
83    #[deprecated(note = "PostgreSQL is no longer supported. Use sqlite() instead.")]
84    pub fn postgres(connection_string: impl Into<String>) -> Self {
85        Self {
86            connection_string: connection_string.into(),
87            db_type: DatabaseType::Postgres,
88            max_connections: default_max_connections(),
89            timeout_secs: default_timeout(),
90            read_only: false,
91            max_rows: default_max_rows(),
92        }
93    }
94
95    /// Set the maximum number of connections
96    pub fn with_max_connections(mut self, max: u32) -> Self {
97        self.max_connections = max;
98        self
99    }
100
101    /// Set read-only mode
102    pub fn with_read_only(mut self, read_only: bool) -> Self {
103        self.read_only = read_only;
104        self
105    }
106
107    /// Set maximum rows to return
108    pub fn with_max_rows(mut self, max_rows: usize) -> Self {
109        self.max_rows = max_rows;
110        self
111    }
112}
113
114/// Query result from database operations
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct QueryResult {
117    /// Column names
118    pub columns: Vec<String>,
119    /// Rows as JSON arrays
120    pub rows: Vec<Vec<Value>>,
121    /// Number of rows returned
122    pub row_count: usize,
123    /// Whether results were truncated due to max_rows limit
124    pub truncated: bool,
125}
126
127/// Execute result from database commands
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct ExecuteResult {
130    /// Number of rows affected
131    pub rows_affected: u64,
132}
133
134/// Transaction result
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct TransactionResult {
137    /// Results from each statement
138    pub statement_results: Vec<StatementResult>,
139    /// Whether the transaction was committed
140    pub committed: bool,
141}
142
143/// Result from a single statement in a transaction
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct StatementResult {
146    /// Statement index (0-based)
147    pub index: usize,
148    /// Number of rows affected (for execute) or returned (for query)
149    pub rows_affected: u64,
150    /// Error message if statement failed
151    pub error: Option<String>,
152}
153
154/// Built-in MCP server for database operations
155#[cfg(feature = "database")]
156pub struct DatabaseServer {
157    /// Database configuration
158    config: DatabaseConfig,
159    /// Connection pool
160    pool: SqlitePool,
161}
162
163#[cfg(not(feature = "database"))]
164pub struct DatabaseServer {
165    /// Database configuration
166    #[allow(dead_code)]
167    config: DatabaseConfig,
168}
169
170impl DatabaseServer {
171    /// Create a new database server (async, requires database feature)
172    #[cfg(feature = "database")]
173    pub async fn new(config: DatabaseConfig) -> Result<Self> {
174        match config.db_type {
175            DatabaseType::Sqlite => {
176                let pool = sqlx::sqlite::SqlitePoolOptions::new()
177                    .max_connections(config.max_connections)
178                    .acquire_timeout(std::time::Duration::from_secs(config.timeout_secs))
179                    .connect(&config.connection_string)
180                    .await
181                    .map_err(|e| {
182                        crate::McpError::ToolExecutionError(format!(
183                            "Failed to connect to database: {}",
184                            e
185                        ))
186                    })?;
187
188                Ok(Self { config, pool })
189            }
190            DatabaseType::Mysql | DatabaseType::Postgres => {
191                Err(crate::McpError::ToolExecutionError(format!(
192                    "{:?} is not yet supported. Only SQLite is currently implemented.",
193                    config.db_type
194                )))
195            }
196        }
197    }
198
199    /// Create a new database server (stub without database feature)
200    #[cfg(not(feature = "database"))]
201    pub fn new(config: DatabaseConfig) -> Self {
202        Self { config }
203    }
204
205    /// Create from an existing pool (useful for testing)
206    #[cfg(feature = "database")]
207    pub fn from_pool(pool: SqlitePool, config: DatabaseConfig) -> Self {
208        Self { config, pool }
209    }
210
211    /// Check if the server is in read-only mode
212    #[allow(dead_code)]
213    fn is_read_only(&self) -> bool {
214        self.config.read_only
215    }
216
217    /// Check if a SQL statement is a mutation (INSERT, UPDATE, DELETE, etc.)
218    #[cfg_attr(not(feature = "database"), allow(dead_code))]
219    fn is_mutation(sql: &str) -> bool {
220        let sql_upper = sql.trim().to_uppercase();
221        sql_upper.starts_with("INSERT")
222            || sql_upper.starts_with("UPDATE")
223            || sql_upper.starts_with("DELETE")
224            || sql_upper.starts_with("DROP")
225            || sql_upper.starts_with("CREATE")
226            || sql_upper.starts_with("ALTER")
227            || sql_upper.starts_with("TRUNCATE")
228    }
229}
230
231#[cfg(feature = "database")]
232#[async_trait]
233impl McpServer for DatabaseServer {
234    async fn call_tool(&self, name: &str, arguments: Value) -> Result<Value> {
235        match name {
236            "db_query" => {
237                let sql_str = arguments
238                    .get("sql")
239                    .and_then(|v| v.as_str())
240                    .ok_or_else(|| {
241                        crate::McpError::InvalidArgument("sql is required".to_string())
242                    })?;
243
244                // Check for mutations in read-only mode
245                if self.config.read_only && Self::is_mutation(sql_str) {
246                    return Err(crate::McpError::ToolExecutionError(
247                        "Mutation queries are not allowed in read-only mode".to_string(),
248                    ));
249                }
250
251                let sql: &'static str = Box::leak(sql_str.to_string().into_boxed_str());
252
253                // Execute the query
254                let rows: Vec<sqlx::sqlite::SqliteRow> =
255                    sqlx::query(sql).fetch_all(&self.pool).await.map_err(|e| {
256                        crate::McpError::ToolExecutionError(format!("Query failed: {}", e))
257                    })?;
258
259                // Extract column names from the first row (if any)
260                let columns: Vec<String> = if let Some(row) = rows.first() {
261                    row.columns().iter().map(|c| c.name().to_string()).collect()
262                } else {
263                    vec![]
264                };
265
266                // Convert rows to JSON
267                let mut result_rows = Vec::new();
268                let truncated = rows.len() > self.config.max_rows;
269
270                for row in rows.iter().take(self.config.max_rows) {
271                    let mut row_values = Vec::new();
272                    for col in row.columns() {
273                        let value = extract_column_value(row, col)?;
274                        row_values.push(value);
275                    }
276                    result_rows.push(row_values);
277                }
278
279                let result = QueryResult {
280                    columns,
281                    rows: result_rows.clone(),
282                    row_count: result_rows.len(),
283                    truncated,
284                };
285
286                Ok(serde_json::to_value(result).map_err(|e| {
287                    crate::McpError::ToolExecutionError(format!(
288                        "Failed to serialize result: {}",
289                        e
290                    ))
291                })?)
292            }
293
294            "db_execute" => {
295                if self.config.read_only {
296                    return Err(crate::McpError::ToolExecutionError(
297                        "Execute commands are not allowed in read-only mode".to_string(),
298                    ));
299                }
300
301                let sql_str = arguments
302                    .get("sql")
303                    .and_then(|v| v.as_str())
304                    .ok_or_else(|| {
305                        crate::McpError::InvalidArgument("sql is required".to_string())
306                    })?;
307
308                let sql: &'static str = Box::leak(sql_str.to_string().into_boxed_str());
309
310                let result: sqlx::sqlite::SqliteQueryResult =
311                    sqlx::query(sql).execute(&self.pool).await.map_err(|e| {
312                        crate::McpError::ToolExecutionError(format!("Execute failed: {}", e))
313                    })?;
314
315                let exec_result = ExecuteResult {
316                    rows_affected: result.rows_affected(),
317                };
318
319                Ok(serde_json::to_value(exec_result).map_err(|e| {
320                    crate::McpError::ToolExecutionError(format!(
321                        "Failed to serialize result: {}",
322                        e
323                    ))
324                })?)
325            }
326
327            "db_transaction" => {
328                if self.config.read_only {
329                    return Err(crate::McpError::ToolExecutionError(
330                        "Transactions are not allowed in read-only mode".to_string(),
331                    ));
332                }
333
334                let statements = arguments
335                    .get("statements")
336                    .and_then(|v| v.as_array())
337                    .ok_or_else(|| {
338                        crate::McpError::InvalidArgument(
339                            "statements is required and must be an array".to_string(),
340                        )
341                    })?;
342
343                let mut tx: sqlx::Transaction<'_, sqlx::Sqlite> =
344                    self.pool.begin().await.map_err(|e| {
345                        crate::McpError::ToolExecutionError(format!(
346                            "Failed to start transaction: {}",
347                            e
348                        ))
349                    })?;
350
351                let mut statement_results = Vec::new();
352
353                for (index, stmt) in statements.iter().enumerate() {
354                    let sql_str = stmt.get("sql").and_then(|v| v.as_str()).ok_or_else(|| {
355                        crate::McpError::InvalidArgument(format!(
356                            "Statement {} is missing sql field",
357                            index
358                        ))
359                    })?;
360
361                    let sql: &'static str = Box::leak(sql_str.to_string().into_boxed_str());
362
363                    match sqlx::query(sql).execute(&mut *tx).await {
364                        Ok(result) => {
365                            statement_results.push(StatementResult {
366                                index,
367                                rows_affected: result.rows_affected(),
368                                error: None,
369                            });
370                        }
371                        Err(e) => {
372                            // Rollback on error
373                            let _ = tx.rollback().await;
374
375                            statement_results.push(StatementResult {
376                                index,
377                                rows_affected: 0,
378                                error: Some(e.to_string()),
379                            });
380
381                            let result = TransactionResult {
382                                statement_results,
383                                committed: false,
384                            };
385
386                            return serde_json::to_value(result).map_err(|e| {
387                                crate::McpError::ToolExecutionError(format!(
388                                    "Failed to serialize result: {}",
389                                    e
390                                ))
391                            });
392                        }
393                    }
394                }
395
396                // Commit transaction
397                tx.commit().await.map_err(|e| {
398                    crate::McpError::ToolExecutionError(format!(
399                        "Failed to commit transaction: {}",
400                        e
401                    ))
402                })?;
403
404                let result = TransactionResult {
405                    statement_results,
406                    committed: true,
407                };
408
409                Ok(serde_json::to_value(result).map_err(|e| {
410                    crate::McpError::ToolExecutionError(format!(
411                        "Failed to serialize result: {}",
412                        e
413                    ))
414                })?)
415            }
416
417            "db_describe" => {
418                let table = arguments
419                    .get("table")
420                    .and_then(|v| v.as_str())
421                    .ok_or_else(|| {
422                        crate::McpError::InvalidArgument("table is required".to_string())
423                    })?;
424
425                // Get column information using SQLite's PRAGMA
426                let sql: &'static str =
427                    Box::leak(format!("PRAGMA table_info({})", table).into_boxed_str());
428
429                let rows: Vec<sqlx::sqlite::SqliteRow> =
430                    sqlx::query(sql).fetch_all(&self.pool).await.map_err(|e| {
431                        crate::McpError::ToolExecutionError(format!("Describe failed: {}", e))
432                    })?;
433
434                let columns: Vec<Value> = rows
435                    .iter()
436                    .map(|row: &sqlx::sqlite::SqliteRow| {
437                        json!({
438                            "column_name": row.get::<String, _>("name"),
439                            "data_type": row.get::<String, _>("type"),
440                            "is_nullable": if row.get::<i32, _>("notnull") == 0 { "YES" } else { "NO" },
441                            "column_default": row.get::<Option<String>, _>("dflt_value")
442                        })
443                    })
444                    .collect();
445
446                Ok(json!({
447                    "table": table,
448                    "columns": columns
449                }))
450            }
451
452            "db_tables" => {
453                let _schema = arguments
454                    .get("schema")
455                    .and_then(|v| v.as_str())
456                    .unwrap_or("main");
457
458                // SQLite uses sqlite_master instead of information_schema
459                let sql = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
460
461                let rows: Vec<sqlx::sqlite::SqliteRow> =
462                    sqlx::query(sql).fetch_all(&self.pool).await.map_err(|e| {
463                        crate::McpError::ToolExecutionError(format!("List tables failed: {}", e))
464                    })?;
465
466                let tables: Vec<String> = rows
467                    .iter()
468                    .map(|row: &sqlx::sqlite::SqliteRow| row.get::<String, _>("name"))
469                    .collect();
470
471                Ok(json!({
472                    "schema": "main",
473                    "tables": tables
474                }))
475            }
476
477            _ => Err(crate::McpError::ToolNotFound(name.to_string())),
478        }
479    }
480
481    async fn list_tools(&self) -> Result<Vec<Value>> {
482        Ok(vec![
483            json!({
484                "name": "db_query",
485                "description": "Execute a SQL SELECT query and return results as JSON",
486                "inputSchema": {
487                    "type": "object",
488                    "properties": {
489                        "sql": {
490                            "type": "string",
491                            "description": "SQL query to execute (SELECT statements)"
492                        }
493                    },
494                    "required": ["sql"]
495                }
496            }),
497            json!({
498                "name": "db_execute",
499                "description": "Execute a SQL command (INSERT, UPDATE, DELETE) and return rows affected",
500                "inputSchema": {
501                    "type": "object",
502                    "properties": {
503                        "sql": {
504                            "type": "string",
505                            "description": "SQL command to execute"
506                        }
507                    },
508                    "required": ["sql"]
509                }
510            }),
511            json!({
512                "name": "db_transaction",
513                "description": "Execute multiple SQL statements in a transaction (atomic)",
514                "inputSchema": {
515                    "type": "object",
516                    "properties": {
517                        "statements": {
518                            "type": "array",
519                            "description": "SQL statements to execute in the transaction",
520                            "items": {
521                                "type": "object",
522                                "properties": {
523                                    "sql": { "type": "string" }
524                                },
525                                "required": ["sql"]
526                            }
527                        }
528                    },
529                    "required": ["statements"]
530                }
531            }),
532            json!({
533                "name": "db_describe",
534                "description": "Get schema information for a table (columns, types)",
535                "inputSchema": {
536                    "type": "object",
537                    "properties": {
538                        "table": {
539                            "type": "string",
540                            "description": "Name of the table to describe"
541                        }
542                    },
543                    "required": ["table"]
544                }
545            }),
546            json!({
547                "name": "db_tables",
548                "description": "List all tables in a schema",
549                "inputSchema": {
550                    "type": "object",
551                    "properties": {
552                        "schema": {
553                            "type": "string",
554                            "description": "Schema name (default: public)",
555                            "default": "public"
556                        }
557                    }
558                }
559            }),
560        ])
561    }
562}
563
564#[cfg(not(feature = "database"))]
565#[async_trait]
566impl McpServer for DatabaseServer {
567    async fn call_tool(&self, name: &str, _arguments: Value) -> Result<Value> {
568        match name {
569            "db_query" | "db_execute" | "db_transaction" | "db_describe" | "db_tables" => {
570                Err(crate::McpError::ToolExecutionError(
571                    "Database operations require the 'database' feature. \
572                     Enable it with: oxify-mcp = { version = \"0.1\", features = [\"database\"] }"
573                        .to_string(),
574                ))
575            }
576            _ => Err(crate::McpError::ToolNotFound(name.to_string())),
577        }
578    }
579
580    async fn list_tools(&self) -> Result<Vec<Value>> {
581        Ok(vec![
582            json!({
583                "name": "db_query",
584                "description": "Execute SQL query (requires 'database' feature)",
585                "inputSchema": {
586                    "type": "object",
587                    "properties": {
588                        "sql": {
589                            "type": "string",
590                            "description": "SQL query to execute"
591                        }
592                    },
593                    "required": ["sql"]
594                }
595            }),
596            json!({
597                "name": "db_execute",
598                "description": "Execute SQL command (requires 'database' feature)",
599                "inputSchema": {
600                    "type": "object",
601                    "properties": {
602                        "sql": {
603                            "type": "string",
604                            "description": "SQL command to execute"
605                        }
606                    },
607                    "required": ["sql"]
608                }
609            }),
610            json!({
611                "name": "db_transaction",
612                "description": "Execute transaction (requires 'database' feature)",
613                "inputSchema": {
614                    "type": "object",
615                    "properties": {
616                        "statements": {
617                            "type": "array",
618                            "description": "SQL statements in transaction",
619                            "items": {
620                                "type": "object",
621                                "properties": {
622                                    "sql": { "type": "string" }
623                                }
624                            }
625                        }
626                    },
627                    "required": ["statements"]
628                }
629            }),
630            json!({
631                "name": "db_describe",
632                "description": "Describe table schema (requires 'database' feature)",
633                "inputSchema": {
634                    "type": "object",
635                    "properties": {
636                        "table": {
637                            "type": "string",
638                            "description": "Table name"
639                        }
640                    },
641                    "required": ["table"]
642                }
643            }),
644            json!({
645                "name": "db_tables",
646                "description": "List tables (requires 'database' feature)",
647                "inputSchema": {
648                    "type": "object",
649                    "properties": {
650                        "schema": {
651                            "type": "string",
652                            "description": "Schema name"
653                        }
654                    }
655                }
656            }),
657        ])
658    }
659}
660
661/// Extract a column value from a row as JSON
662#[cfg(feature = "database")]
663fn extract_column_value(
664    row: &sqlx::sqlite::SqliteRow,
665    col: &sqlx::sqlite::SqliteColumn,
666) -> Result<Value> {
667    use sqlx::ValueRef;
668
669    // Check if the value is null first
670    if row
671        .try_get_raw(col.ordinal())
672        .map(|v: sqlx::sqlite::SqliteValueRef<'_>| v.is_null())
673        .unwrap_or(true)
674    {
675        return Ok(Value::Null);
676    }
677
678    let type_info = col.type_info();
679    let type_name = type_info.name();
680
681    // SQLite has simpler type system: NULL, INTEGER, REAL, TEXT, BLOB
682    match type_name {
683        "BOOLEAN" | "BOOL" => {
684            let v: bool = row.try_get(col.ordinal()).map_err(|e| {
685                crate::McpError::ToolExecutionError(format!("Failed to get bool: {}", e))
686            })?;
687            Ok(Value::Bool(v))
688        }
689        "INTEGER" | "INT" | "BIGINT" | "SMALLINT" => {
690            let v: i64 = row.try_get(col.ordinal()).map_err(|e| {
691                crate::McpError::ToolExecutionError(format!("Failed to get i64: {}", e))
692            })?;
693            Ok(Value::Number(v.into()))
694        }
695        "REAL" | "DOUBLE" | "FLOAT" => {
696            let v: f64 = row.try_get(col.ordinal()).map_err(|e| {
697                crate::McpError::ToolExecutionError(format!("Failed to get f64: {}", e))
698            })?;
699            Ok(serde_json::Number::from_f64(v)
700                .map(Value::Number)
701                .unwrap_or(Value::Null))
702        }
703        "TEXT" | "VARCHAR" | "CHAR" => {
704            let v: String = row.try_get(col.ordinal()).map_err(|e| {
705                crate::McpError::ToolExecutionError(format!("Failed to get string: {}", e))
706            })?;
707            // Try to parse as JSON if it looks like JSON
708            if v.starts_with('{') || v.starts_with('[') {
709                if let Ok(json_val) = serde_json::from_str::<Value>(&v) {
710                    return Ok(json_val);
711                }
712            }
713            Ok(Value::String(v))
714        }
715        "BLOB" => {
716            let v: Vec<u8> = row.try_get(col.ordinal()).map_err(|e| {
717                crate::McpError::ToolExecutionError(format!("Failed to get bytes: {}", e))
718            })?;
719            Ok(Value::String(base64::Engine::encode(
720                &base64::prelude::BASE64_STANDARD,
721                &v,
722            )))
723        }
724        _ => {
725            // Try to get as string for unknown types
726            let v: String = row
727                .try_get(col.ordinal())
728                .unwrap_or_else(|_| format!("<unsupported type: {}>", type_name));
729            Ok(Value::String(v))
730        }
731    }
732}
733
734#[cfg(all(test, feature = "database"))]
735mod tests {
736    use super::*;
737
738    #[test]
739    fn test_database_config() {
740        let config = DatabaseConfig::sqlite("sqlite:test.db")
741            .with_max_connections(10)
742            .with_read_only(true)
743            .with_max_rows(500);
744
745        assert_eq!(config.max_connections, 10);
746        assert!(config.read_only);
747        assert_eq!(config.max_rows, 500);
748        assert_eq!(config.db_type, DatabaseType::Sqlite);
749    }
750
751    #[test]
752    fn test_is_mutation() {
753        assert!(DatabaseServer::is_mutation(
754            "INSERT INTO users (name) VALUES ('test')"
755        ));
756        assert!(DatabaseServer::is_mutation(
757            "UPDATE users SET name = 'test'"
758        ));
759        assert!(DatabaseServer::is_mutation(
760            "DELETE FROM users WHERE id = 1"
761        ));
762        assert!(DatabaseServer::is_mutation("DROP TABLE users"));
763        assert!(DatabaseServer::is_mutation("CREATE TABLE users (id INT)"));
764        assert!(DatabaseServer::is_mutation(
765            "ALTER TABLE users ADD COLUMN age INT"
766        ));
767        assert!(DatabaseServer::is_mutation("TRUNCATE TABLE users"));
768
769        assert!(!DatabaseServer::is_mutation("SELECT * FROM users"));
770        assert!(!DatabaseServer::is_mutation("  SELECT id FROM users"));
771    }
772
773    #[test]
774    fn test_query_result_serialization() {
775        let result = QueryResult {
776            columns: vec!["id".to_string(), "name".to_string()],
777            rows: vec![
778                vec![Value::Number(1.into()), Value::String("Alice".to_string())],
779                vec![Value::Number(2.into()), Value::String("Bob".to_string())],
780            ],
781            row_count: 2,
782            truncated: false,
783        };
784
785        let json = serde_json::to_string(&result).unwrap();
786        let parsed: QueryResult = serde_json::from_str(&json).unwrap();
787
788        assert_eq!(parsed.columns, result.columns);
789        assert_eq!(parsed.row_count, 2);
790        assert!(!parsed.truncated);
791    }
792
793    #[test]
794    fn test_execute_result_serialization() {
795        let result = ExecuteResult { rows_affected: 5 };
796        let json = serde_json::to_string(&result).unwrap();
797        let parsed: ExecuteResult = serde_json::from_str(&json).unwrap();
798
799        assert_eq!(parsed.rows_affected, 5);
800    }
801
802    #[test]
803    fn test_transaction_result_serialization() {
804        let result = TransactionResult {
805            statement_results: vec![
806                StatementResult {
807                    index: 0,
808                    rows_affected: 1,
809                    error: None,
810                },
811                StatementResult {
812                    index: 1,
813                    rows_affected: 2,
814                    error: None,
815                },
816            ],
817            committed: true,
818        };
819
820        let json = serde_json::to_string(&result).unwrap();
821        let parsed: TransactionResult = serde_json::from_str(&json).unwrap();
822
823        assert!(parsed.committed);
824        assert_eq!(parsed.statement_results.len(), 2);
825    }
826
827    #[test]
828    fn test_database_type_default() {
829        let db_type = DatabaseType::default();
830        assert_eq!(db_type, DatabaseType::Sqlite);
831    }
832}
833
834#[cfg(all(test, not(feature = "database")))]
835mod tests_no_feature {
836    use super::*;
837
838    #[test]
839    fn test_database_config() {
840        let config = DatabaseConfig::sqlite("sqlite:test.db")
841            .with_max_connections(10)
842            .with_read_only(true)
843            .with_max_rows(500);
844
845        assert_eq!(config.max_connections, 10);
846        assert!(config.read_only);
847        assert_eq!(config.max_rows, 500);
848        assert_eq!(config.db_type, DatabaseType::Sqlite);
849    }
850
851    #[test]
852    fn test_is_mutation() {
853        assert!(DatabaseServer::is_mutation(
854            "INSERT INTO users (name) VALUES ('test')"
855        ));
856        assert!(DatabaseServer::is_mutation(
857            "UPDATE users SET name = 'test'"
858        ));
859        assert!(!DatabaseServer::is_mutation("SELECT * FROM users"));
860    }
861
862    #[tokio::test]
863    async fn test_stub_returns_feature_error() {
864        let config = DatabaseConfig::sqlite("sqlite::memory:");
865        let server = DatabaseServer::new(config);
866
867        let result = server
868            .call_tool("db_query", json!({"sql": "SELECT 1"}))
869            .await;
870        assert!(result.is_err());
871
872        let err = result.unwrap_err();
873        assert!(err.to_string().contains("database"));
874    }
875
876    #[tokio::test]
877    async fn test_list_tools_without_feature() {
878        let config = DatabaseConfig::sqlite("sqlite::memory:");
879        let server = DatabaseServer::new(config);
880
881        let tools = server.list_tools().await.unwrap();
882        assert_eq!(tools.len(), 5);
883
884        let names: Vec<&str> = tools
885            .iter()
886            .filter_map(|t| t.get("name").and_then(|n| n.as_str()))
887            .collect();
888
889        assert!(names.contains(&"db_query"));
890        assert!(names.contains(&"db_execute"));
891        assert!(names.contains(&"db_transaction"));
892        assert!(names.contains(&"db_describe"));
893        assert!(names.contains(&"db_tables"));
894    }
895}