1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25#[derive(Default)]
26pub enum DatabaseType {
27 Postgres,
29 Mysql,
31 #[default]
33 Sqlite,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct DatabaseConfig {
39 pub connection_string: String,
41 pub db_type: DatabaseType,
43 #[serde(default = "default_max_connections")]
45 pub max_connections: u32,
46 #[serde(default = "default_timeout")]
48 pub timeout_secs: u64,
49 #[serde(default)]
51 pub read_only: bool,
52 #[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 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 #[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 pub fn with_max_connections(mut self, max: u32) -> Self {
97 self.max_connections = max;
98 self
99 }
100
101 pub fn with_read_only(mut self, read_only: bool) -> Self {
103 self.read_only = read_only;
104 self
105 }
106
107 pub fn with_max_rows(mut self, max_rows: usize) -> Self {
109 self.max_rows = max_rows;
110 self
111 }
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct QueryResult {
117 pub columns: Vec<String>,
119 pub rows: Vec<Vec<Value>>,
121 pub row_count: usize,
123 pub truncated: bool,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct ExecuteResult {
130 pub rows_affected: u64,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct TransactionResult {
137 pub statement_results: Vec<StatementResult>,
139 pub committed: bool,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct StatementResult {
146 pub index: usize,
148 pub rows_affected: u64,
150 pub error: Option<String>,
152}
153
154#[cfg(feature = "database")]
156pub struct DatabaseServer {
157 config: DatabaseConfig,
159 pool: SqlitePool,
161}
162
163#[cfg(not(feature = "database"))]
164pub struct DatabaseServer {
165 #[allow(dead_code)]
167 config: DatabaseConfig,
168}
169
170impl DatabaseServer {
171 #[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 #[cfg(not(feature = "database"))]
201 pub fn new(config: DatabaseConfig) -> Self {
202 Self { config }
203 }
204
205 #[cfg(feature = "database")]
207 pub fn from_pool(pool: SqlitePool, config: DatabaseConfig) -> Self {
208 Self { config, pool }
209 }
210
211 #[allow(dead_code)]
213 fn is_read_only(&self) -> bool {
214 self.config.read_only
215 }
216
217 #[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 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 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 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 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 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 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 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 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#[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 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 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 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 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}