mcp-dbutils 0.20.0__tar.gz → 0.20.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/CHANGELOG.md +7 -0
  2. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/PKG-INFO +1 -1
  3. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/pyproject.toml +1 -1
  4. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/src/mcp_dbutils/mysql/handler.py +2 -0
  5. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/src/mcp_dbutils/postgres/handler.py +2 -0
  6. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/unit/test_mysql_handler.py +207 -110
  7. mcp_dbutils-0.20.1/tests/unit/test_postgres_handler.py +268 -0
  8. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.coveragerc +0 -0
  9. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/bug_report_ar.md +0 -0
  10. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/bug_report_en.md +0 -0
  11. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/bug_report_es.md +0 -0
  12. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/bug_report_fr.md +0 -0
  13. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/bug_report_ru.md +0 -0
  14. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/bug_report_zh.md +0 -0
  15. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  16. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/documentation_improvement_ar.md +0 -0
  17. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/documentation_improvement_en.md +0 -0
  18. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/documentation_improvement_es.md +0 -0
  19. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/documentation_improvement_fr.md +0 -0
  20. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/documentation_improvement_ru.md +0 -0
  21. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/documentation_improvement_zh.md +0 -0
  22. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/feature_request_ar.md +0 -0
  23. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/feature_request_en.md +0 -0
  24. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/feature_request_es.md +0 -0
  25. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/feature_request_fr.md +0 -0
  26. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/feature_request_ru.md +0 -0
  27. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/feature_request_zh.md +0 -0
  28. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/performance_issue_ar.md +0 -0
  29. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/performance_issue_en.md +0 -0
  30. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/performance_issue_es.md +0 -0
  31. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/performance_issue_fr.md +0 -0
  32. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/performance_issue_ru.md +0 -0
  33. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/performance_issue_zh.md +0 -0
  34. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/security_vulnerability_ar.md +0 -0
  35. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/security_vulnerability_en.md +0 -0
  36. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/security_vulnerability_es.md +0 -0
  37. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/security_vulnerability_fr.md +0 -0
  38. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/security_vulnerability_ru.md +0 -0
  39. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/ISSUE_TEMPLATE/security_vulnerability_zh.md +0 -0
  40. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/workflows/code-style.yml +0 -0
  41. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/workflows/issue-translator.yml +0 -0
  42. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/workflows/quality-assurance.yml +0 -0
  43. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.github/workflows/release.yml +0 -0
  44. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.gitignore +0 -0
  45. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.pre-commit-config.yaml +0 -0
  46. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/.releaserc.json +0 -0
  47. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/Dockerfile +0 -0
  48. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/LICENSE +0 -0
  49. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/README.md +0 -0
  50. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/README_AR.md +0 -0
  51. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/README_EN.md +0 -0
  52. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/README_ES.md +0 -0
  53. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/README_FR.md +0 -0
  54. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/README_RU.md +0 -0
  55. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/config.yaml.example +0 -0
  56. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ar/configuration.md +0 -0
  57. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ar/examples/README.md +0 -0
  58. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ar/examples/advanced-llm-interactions.md +0 -0
  59. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ar/examples/mysql-examples.md +0 -0
  60. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ar/examples/postgresql-examples.md +0 -0
  61. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ar/examples/sqlite-examples.md +0 -0
  62. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ar/installation-platform-specific.md +0 -0
  63. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ar/installation.md +0 -0
  64. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ar/technical/architecture.md +0 -0
  65. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ar/technical/development.md +0 -0
  66. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ar/technical/security.md +0 -0
  67. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ar/technical/sonarcloud-integration.md +0 -0
  68. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ar/technical/testing.md +0 -0
  69. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ar/usage.md +0 -0
  70. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/document-consistency-check.md +0 -0
  71. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/document-version-history.md +0 -0
  72. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/en/configuration.md +0 -0
  73. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/en/examples/advanced-llm-interactions.md +0 -0
  74. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/en/examples/mysql-examples.md +0 -0
  75. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/en/examples/postgresql-examples.md +0 -0
  76. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/en/examples/sqlite-examples.md +0 -0
  77. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/en/installation-platform-specific.md +0 -0
  78. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/en/installation.md +0 -0
  79. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/en/technical/architecture.md +0 -0
  80. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/en/technical/development.md +0 -0
  81. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/en/technical/security.md +0 -0
  82. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/en/technical/sonarcloud-integration.md +0 -0
  83. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/en/technical/testing.md +0 -0
  84. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/en/usage.md +0 -0
  85. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/es/configuration.md +0 -0
  86. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/es/examples/README.md +0 -0
  87. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/es/examples/advanced-llm-interactions.md +0 -0
  88. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/es/examples/mysql-examples.md +0 -0
  89. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/es/examples/postgresql-examples.md +0 -0
  90. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/es/examples/sqlite-examples.md +0 -0
  91. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/es/installation-platform-specific.md +0 -0
  92. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/es/installation.md +0 -0
  93. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/es/technical/architecture.md +0 -0
  94. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/es/technical/development.md +0 -0
  95. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/es/technical/security.md +0 -0
  96. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/es/technical/sonarcloud-integration.md +0 -0
  97. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/es/technical/testing.md +0 -0
  98. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/es/usage.md +0 -0
  99. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/fr/configuration.md +0 -0
  100. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/fr/examples/README.md +0 -0
  101. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/fr/examples/advanced-llm-interactions.md +0 -0
  102. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/fr/examples/mysql-examples.md +0 -0
  103. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/fr/examples/postgresql-examples.md +0 -0
  104. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/fr/examples/sqlite-examples.md +0 -0
  105. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/fr/installation-platform-specific.md +0 -0
  106. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/fr/installation.md +0 -0
  107. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/fr/technical/architecture.md +0 -0
  108. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/fr/technical/development.md +0 -0
  109. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/fr/technical/security.md +0 -0
  110. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/fr/technical/sonarcloud-integration.md +0 -0
  111. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/fr/technical/testing.md +0 -0
  112. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/fr/usage.md +0 -0
  113. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/index.md +0 -0
  114. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ru/configuration.md +0 -0
  115. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ru/examples/README.md +0 -0
  116. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ru/examples/advanced-llm-interactions.md +0 -0
  117. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ru/examples/mysql-examples.md +0 -0
  118. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ru/examples/postgresql-examples.md +0 -0
  119. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ru/examples/sqlite-examples.md +0 -0
  120. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ru/installation-platform-specific.md +0 -0
  121. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ru/installation.md +0 -0
  122. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ru/technical/architecture.md +0 -0
  123. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ru/technical/development.md +0 -0
  124. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ru/technical/security.md +0 -0
  125. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ru/technical/sonarcloud-integration.md +0 -0
  126. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ru/technical/testing.md +0 -0
  127. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/ru/usage.md +0 -0
  128. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/zh/configuration.md +0 -0
  129. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/zh/examples/advanced-llm-interactions.md +0 -0
  130. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/zh/examples/mysql-examples.md +0 -0
  131. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/zh/examples/postgresql-examples.md +0 -0
  132. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/zh/examples/sqlite-examples.md +0 -0
  133. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/zh/installation-platform-specific.md +0 -0
  134. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/zh/installation.md +0 -0
  135. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/zh/technical/architecture.md +0 -0
  136. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/zh/technical/development.md +0 -0
  137. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/zh/technical/security.md +0 -0
  138. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/zh/technical/sonarcloud-integration.md +0 -0
  139. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/zh/technical/testing.md +0 -0
  140. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/docs/zh/usage.md +0 -0
  141. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/scripts/check_docs_consistency.py +0 -0
  142. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/scripts/check_zh_docs.py +0 -0
  143. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/scripts/fix_en_nav_links.py +0 -0
  144. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/scripts/fix_imports.sh +0 -0
  145. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/scripts/fix_remaining_issues.sh +0 -0
  146. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/scripts/fix_zh_nav_links.py +0 -0
  147. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/scripts/run_sonar_analysis.sh +0 -0
  148. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/scripts/sonar-ai-fix.fish +0 -0
  149. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/smithery.yaml +0 -0
  150. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/sonar-project.properties +0 -0
  151. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/src/mcp_dbutils/__init__.py +0 -0
  152. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/src/mcp_dbutils/base.py +0 -0
  153. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/src/mcp_dbutils/config.py +0 -0
  154. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/src/mcp_dbutils/log.py +0 -0
  155. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/src/mcp_dbutils/mysql/__init__.py +0 -0
  156. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/src/mcp_dbutils/mysql/config.py +0 -0
  157. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/src/mcp_dbutils/mysql/server.py +0 -0
  158. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/src/mcp_dbutils/postgres/__init__.py +0 -0
  159. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/src/mcp_dbutils/postgres/config.py +0 -0
  160. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/src/mcp_dbutils/postgres/server.py +0 -0
  161. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/src/mcp_dbutils/sqlite/__init__.py +0 -0
  162. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/src/mcp_dbutils/sqlite/config.py +0 -0
  163. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/src/mcp_dbutils/sqlite/handler.py +0 -0
  164. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/src/mcp_dbutils/sqlite/server.py +0 -0
  165. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/src/mcp_dbutils/stats.py +0 -0
  166. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/conftest.py +0 -0
  167. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/__init__.py +0 -0
  168. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/conftest.py +0 -0
  169. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/fixtures.py +0 -0
  170. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/test_list_connections.py +0 -0
  171. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/test_logging.py +0 -0
  172. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/test_monitoring.py +0 -0
  173. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/test_monitoring_enhanced.py +0 -0
  174. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/test_mysql.py +0 -0
  175. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/test_mysql_config.py +0 -0
  176. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/test_mysql_config_helpers.py +0 -0
  177. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/test_mysql_handler_extended.py +0 -0
  178. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/test_postgres.py +0 -0
  179. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/test_postgres_config.py +0 -0
  180. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/test_prompts.py +0 -0
  181. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/test_sqlite.py +0 -0
  182. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/test_sqlite_config.py +0 -0
  183. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/test_sqlite_handler_extended.py +0 -0
  184. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/test_tools.py +0 -0
  185. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/integration/test_tools_advanced.py +0 -0
  186. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/unit/test_base.py +0 -0
  187. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/unit/test_base_handlers.py +0 -0
  188. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/unit/test_base_helpers.py +0 -0
  189. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/unit/test_base_server.py +0 -0
  190. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/unit/test_log.py +0 -0
  191. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/unit/test_mysql_server.py +0 -0
  192. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/unit/test_postgres_server.py +0 -0
  193. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/unit/test_sqlite_server.py +0 -0
  194. {mcp_dbutils-0.20.0 → mcp_dbutils-0.20.1}/tests/unit/test_stats.py +0 -0
@@ -1,3 +1,10 @@
1
+ ## [0.20.1](https://github.com/donghao1393/mcp-dbutils/compare/v0.20.0...v0.20.1) (2025-04-30)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * 修复MySQL和PostgreSQL处理器中变量作用域问题 ([#78](https://github.com/donghao1393/mcp-dbutils/issues/78)) ([9479e67](https://github.com/donghao1393/mcp-dbutils/commit/9479e677fb78346b6ef34ad474055f4df787a059)), closes [#75](https://github.com/donghao1393/mcp-dbutils/issues/75) [#75](https://github.com/donghao1393/mcp-dbutils/issues/75)
7
+
1
8
  # [0.20.0](https://github.com/donghao1393/mcp-dbutils/compare/v0.19.0...v0.20.0) (2025-04-30)
2
9
 
3
10
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-dbutils
3
- Version: 0.20.0
3
+ Version: 0.20.1
4
4
  Summary: MCP Database Utilities Service
5
5
  Author: Dong Hao
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcp-dbutils"
3
- version = "0.20.0"
3
+ version = "0.20.1"
4
4
  description = "MCP Database Utilities Service"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -62,6 +62,7 @@ class MySQLHandler(ConnectionHandler):
62
62
 
63
63
  async def get_tables(self) -> list[types.Resource]:
64
64
  """Get all table resources"""
65
+ conn = None
65
66
  try:
66
67
  conn_params = self.config.get_connection_params()
67
68
  conn = mysql.connector.connect(**conn_params)
@@ -92,6 +93,7 @@ class MySQLHandler(ConnectionHandler):
92
93
 
93
94
  async def get_schema(self, table_name: str) -> str:
94
95
  """Get table schema information"""
96
+ conn = None
95
97
  try:
96
98
  conn_params = self.config.get_connection_params()
97
99
  conn = mysql.connector.connect(**conn_params)
@@ -33,6 +33,7 @@ class PostgreSQLHandler(ConnectionHandler):
33
33
 
34
34
  async def get_tables(self) -> list[types.Resource]:
35
35
  """Get all table resources"""
36
+ conn = None
36
37
  try:
37
38
  conn_params = self.config.get_connection_params()
38
39
  conn = psycopg2.connect(**conn_params)
@@ -66,6 +67,7 @@ class PostgreSQLHandler(ConnectionHandler):
66
67
 
67
68
  async def get_schema(self, table_name: str) -> str:
68
69
  """Get table schema information"""
70
+ conn = None
69
71
  try:
70
72
  conn_params = self.config.get_connection_params()
71
73
  conn = psycopg2.connect(**conn_params)
@@ -61,17 +61,17 @@ class TestMySQLHandler:
61
61
  {'table_name': 'users', 'description': 'User table'},
62
62
  {'table_name': 'orders', 'description': None}
63
63
  ]
64
-
64
+
65
65
  # Call the method
66
66
  result = await handler.get_tables()
67
-
67
+
68
68
  # Verify connection was made with correct parameters
69
69
  mock_connect.assert_called_once()
70
-
70
+
71
71
  # Verify the cursor was used correctly
72
72
  mock_conn.cursor().__enter__().execute.assert_called_once()
73
73
  mock_conn.cursor().__enter__().fetchall.assert_called_once()
74
-
74
+
75
75
  # Verify the result format
76
76
  assert isinstance(result, list)
77
77
  assert len(result) == 2
@@ -80,35 +80,21 @@ class TestMySQLHandler:
80
80
  assert result[0].description == 'User table'
81
81
  assert result[1].name == 'orders schema'
82
82
  assert result[1].description is None
83
-
83
+
84
84
  # Verify connection was closed
85
85
  mock_conn.close.assert_called_once()
86
86
 
87
87
  @pytest.mark.asyncio
88
- async def test_get_tables_error(self, handler, mock_conn):
89
- """Test error handling when getting tables"""
90
- # Save the original method
91
- original_get_tables = handler.get_tables
92
-
93
- # Replace with a method that raises the expected exception
94
- async def mock_get_tables():
95
- handler.stats.record_error.return_value = None
96
- handler.stats.record_error("Error")
97
- raise ConnectionHandlerError("Failed to get tables: Connection failed")
98
-
99
- # Set the mock method
100
- handler.get_tables = mock_get_tables
101
-
102
- try:
88
+ async def test_get_tables_error(self, handler):
89
+ """Test error handling when getting tables with connection failure"""
90
+ # Mock the connector.connect function to raise an exception
91
+ with patch('mysql.connector.connect', side_effect=mysql.connector.Error('Connection failed')):
103
92
  # Call the method and expect an exception
104
93
  with pytest.raises(ConnectionHandlerError, match="Failed to get tables"):
105
94
  await handler.get_tables()
106
-
95
+
107
96
  # Verify error was recorded
108
97
  handler.stats.record_error.assert_called_once()
109
- finally:
110
- # Restore original method
111
- handler.get_tables = original_get_tables
112
98
 
113
99
  @pytest.mark.asyncio
114
100
  async def test_get_schema(self, handler, mock_conn):
@@ -123,53 +109,39 @@ class TestMySQLHandler:
123
109
  constraints = [
124
110
  {'constraint_name': 'PRIMARY', 'constraint_type': 'PRIMARY KEY'}
125
111
  ]
126
-
112
+
127
113
  # Set up the mock cursor to return different data for different queries
128
114
  mock_cursor = mock_conn.cursor().__enter__()
129
115
  mock_cursor.fetchall.side_effect = [columns, constraints]
130
-
116
+
131
117
  # Call the method
132
118
  result = await handler.get_schema('users')
133
-
119
+
134
120
  # Verify connection was made with correct parameters
135
121
  mock_connect.assert_called_once()
136
-
122
+
137
123
  # Verify the cursor was used correctly for both queries
138
124
  assert mock_cursor.execute.call_count == 2
139
-
125
+
140
126
  # Verify the result format (it should be a string representation of dict)
141
127
  assert isinstance(result, str)
142
128
  assert "'columns':" in result
143
129
  assert "'constraints':" in result
144
-
130
+
145
131
  # Verify connection was closed
146
132
  mock_conn.close.assert_called_once()
147
133
 
148
134
  @pytest.mark.asyncio
149
- async def test_get_schema_error(self, handler, mock_conn):
150
- """Test error handling when getting schema"""
151
- # Save the original method
152
- original_get_schema = handler.get_schema
153
-
154
- # Replace with a method that raises the expected exception
155
- async def mock_get_schema(table_name):
156
- handler.stats.record_error.return_value = None
157
- handler.stats.record_error("Error")
158
- raise ConnectionHandlerError("Failed to read table schema: Connection failed")
159
-
160
- # Set the mock method
161
- handler.get_schema = mock_get_schema
162
-
163
- try:
135
+ async def test_get_schema_error(self, handler):
136
+ """Test error handling when getting schema with connection failure"""
137
+ # Mock the connector.connect function to raise an exception
138
+ with patch('mysql.connector.connect', side_effect=mysql.connector.Error('Connection failed')):
164
139
  # Call the method and expect an exception
165
140
  with pytest.raises(ConnectionHandlerError, match="Failed to read table schema"):
166
141
  await handler.get_schema('users')
167
-
142
+
168
143
  # Verify error was recorded
169
144
  handler.stats.record_error.assert_called_once()
170
- finally:
171
- # Restore original method
172
- handler.get_schema = original_get_schema
173
145
 
174
146
  @pytest.mark.asyncio
175
147
  async def test_execute_query(self, handler, mock_conn):
@@ -183,22 +155,22 @@ class TestMySQLHandler:
183
155
  {'id': 1, 'name': 'Test User'},
184
156
  {'id': 2, 'name': 'Another User'}
185
157
  ]
186
-
158
+
187
159
  # Call the method
188
160
  result = await handler._execute_query('SELECT * FROM users')
189
-
161
+
190
162
  # Verify connection was made
191
163
  mock_connect.assert_called_once()
192
-
164
+
193
165
  # Verify the cursor was used correctly
194
166
  assert mock_cursor.execute.call_count == 2 # SET TRANSACTION + Query (不再需要ROLLBACK)
195
167
  mock_cursor.fetchall.assert_called_once()
196
-
168
+
197
169
  # Verify the result format
198
170
  assert isinstance(result, str)
199
171
  assert "columns" in result
200
172
  assert "rows" in result
201
-
173
+
202
174
  # Verify connection was closed
203
175
  mock_conn.close.assert_called_once()
204
176
 
@@ -210,11 +182,11 @@ class TestMySQLHandler:
210
182
  # Mock cursor to raise an exception when executing the query
211
183
  mock_cursor = mock_conn.cursor().__enter__()
212
184
  mock_cursor.execute.side_effect = [None, mysql.connector.Error('Query failed'), None]
213
-
185
+
214
186
  # Call the method and expect an exception
215
187
  with pytest.raises(ConnectionHandlerError, match="Query failed"):
216
188
  await handler._execute_query('SELECT * FROM users')
217
-
189
+
218
190
  # Verify connection was closed even after an error
219
191
  mock_conn.close.assert_called_once()
220
192
 
@@ -244,13 +216,13 @@ class TestMySQLHandler:
244
216
  ]
245
217
  mock_cursor.fetchone.side_effect = [{'count': 1}, table_info]
246
218
  mock_cursor.fetchall.return_value = column_info
247
-
219
+
248
220
  # Call the method
249
221
  result = await handler.get_table_description('users')
250
-
222
+
251
223
  # Verify connection was made
252
224
  mock_connect.assert_called_once()
253
-
225
+
254
226
  # Verify the result format
255
227
  assert isinstance(result, str)
256
228
  assert "Table: users" in result
@@ -258,7 +230,7 @@ class TestMySQLHandler:
258
230
  assert "id" in result
259
231
  assert "name" in result
260
232
  assert "varchar" in result
261
-
233
+
262
234
  # Verify connection was closed
263
235
  mock_conn.close.assert_called_once()
264
236
 
@@ -270,7 +242,7 @@ class TestMySQLHandler:
270
242
  # Call the method and expect an exception
271
243
  with pytest.raises(ConnectionHandlerError, match="Failed to get table description"):
272
244
  await handler.get_table_description('users')
273
-
245
+
274
246
  # Verify error was recorded
275
247
  handler.stats.record_error.assert_called_once()
276
248
 
@@ -284,20 +256,20 @@ class TestMySQLHandler:
284
256
  mock_cursor.fetchone.return_value = {
285
257
  'Create Table': 'CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))'
286
258
  }
287
-
259
+
288
260
  # Call the method
289
261
  result = await handler.get_table_ddl('users')
290
-
262
+
291
263
  # Verify connection was made
292
264
  mock_connect.assert_called_once()
293
-
265
+
294
266
  # Verify the cursor was used correctly
295
267
  mock_cursor.execute.assert_called_once_with('SHOW CREATE TABLE users')
296
268
  mock_cursor.fetchone.assert_called_once()
297
-
269
+
298
270
  # Verify the result format
299
271
  assert result == 'CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))'
300
-
272
+
301
273
  # Verify connection was closed
302
274
  mock_conn.close.assert_called_once()
303
275
 
@@ -309,13 +281,13 @@ class TestMySQLHandler:
309
281
  # Mock cursor to return no DDL
310
282
  mock_cursor = mock_conn.cursor().__enter__()
311
283
  mock_cursor.fetchone.return_value = None
312
-
284
+
313
285
  # Call the method
314
286
  result = await handler.get_table_ddl('users')
315
-
287
+
316
288
  # Verify the result format
317
289
  assert result == 'Failed to get DDL for table users'
318
-
290
+
319
291
  # Verify connection was closed
320
292
  mock_conn.close.assert_called_once()
321
293
 
@@ -327,7 +299,7 @@ class TestMySQLHandler:
327
299
  # Call the method and expect an exception
328
300
  with pytest.raises(ConnectionHandlerError, match="Failed to get table DDL"):
329
301
  await handler.get_table_ddl('users')
330
-
302
+
331
303
  # Verify error was recorded
332
304
  handler.stats.record_error.assert_called_once()
333
305
 
@@ -353,28 +325,28 @@ class TestMySQLHandler:
353
325
  'index_comment': 'Name index'
354
326
  }
355
327
  ]
356
-
328
+
357
329
  mock_cursor = mock_conn.cursor().__enter__()
358
330
  # 首先模拟表存在性检查
359
331
  mock_cursor.fetchone.side_effect = [{'count': 1}]
360
332
  mock_cursor.fetchall.return_value = indexes
361
-
333
+
362
334
  # Call the method
363
335
  result = await handler.get_table_indexes('users')
364
-
336
+
365
337
  # Verify connection was made
366
338
  mock_connect.assert_called_once()
367
-
339
+
368
340
  # Verify the cursor was used correctly
369
341
  assert mock_cursor.execute.call_count == 2 # 表存在性检查 + 索引查询
370
-
342
+
371
343
  # Verify the result format
372
344
  assert isinstance(result, str)
373
345
  assert "Index: PRIMARY" in result
374
346
  assert "Type: UNIQUE" in result
375
347
  assert "Method: BTREE" in result
376
348
  assert "Columns:" in result
377
-
349
+
378
350
  # Verify connection was closed
379
351
  mock_conn.close.assert_called_once()
380
352
 
@@ -388,13 +360,13 @@ class TestMySQLHandler:
388
360
  # 首先模拟表存在性检查返回成功
389
361
  mock_cursor.fetchone.side_effect = [{'count': 1}]
390
362
  mock_cursor.fetchall.return_value = []
391
-
363
+
392
364
  # Call the method
393
365
  result = await handler.get_table_indexes('users')
394
-
366
+
395
367
  # Verify the result format
396
368
  assert result == "No indexes found on table users"
397
-
369
+
398
370
  # Verify connection was closed
399
371
  mock_conn.close.assert_called_once()
400
372
 
@@ -406,7 +378,7 @@ class TestMySQLHandler:
406
378
  # Call the method and expect an exception
407
379
  with pytest.raises(ConnectionHandlerError, match="Failed to get index information"):
408
380
  await handler.get_table_indexes('users')
409
-
381
+
410
382
  # Verify error was recorded
411
383
  handler.stats.record_error.assert_called_once()
412
384
 
@@ -427,29 +399,29 @@ class TestMySQLHandler:
427
399
  {'column_name': 'id', 'data_type': 'int', 'column_type': 'int(11)'},
428
400
  {'column_name': 'name', 'data_type': 'varchar', 'column_type': 'varchar(255)'}
429
401
  ]
430
-
402
+
431
403
  # Set up the mock cursor to return different data for different queries
432
404
  mock_cursor = mock_conn.cursor().__enter__()
433
405
  # 首先模拟表存在性检查返回成功
434
406
  mock_cursor.fetchone.side_effect = [{'count': 1}, table_stats]
435
407
  mock_cursor.fetchall.return_value = columns
436
-
408
+
437
409
  # Call the method
438
410
  result = await handler.get_table_stats('users')
439
-
411
+
440
412
  # Verify connection was made
441
413
  mock_connect.assert_called_once()
442
-
414
+
443
415
  # Verify the cursor was used correctly
444
416
  assert mock_cursor.execute.call_count == 3 # 表存在性检查 + 表统计查询 + 列信息查询
445
-
417
+
446
418
  # Verify the result format
447
419
  assert isinstance(result, str)
448
420
  assert "Table Statistics for users:" in result
449
421
  assert "Estimated Row Count: 1,000" in result
450
422
  assert "Data Length: 100,000" in result
451
423
  assert "Average Row Length: 100" in result
452
-
424
+
453
425
  # Verify connection was closed
454
426
  mock_conn.close.assert_called_once()
455
427
 
@@ -462,13 +434,13 @@ class TestMySQLHandler:
462
434
  mock_cursor = mock_conn.cursor().__enter__()
463
435
  # 首先模拟表存在性检查返回成功,但表统计查询返回空
464
436
  mock_cursor.fetchone.side_effect = [{'count': 1}, None]
465
-
437
+
466
438
  # Call the method
467
439
  result = await handler.get_table_stats('users')
468
-
440
+
469
441
  # Verify the result format
470
442
  assert result == 'No statistics found for table users'
471
-
443
+
472
444
  # Verify connection was closed
473
445
  mock_conn.close.assert_called_once()
474
446
 
@@ -480,7 +452,7 @@ class TestMySQLHandler:
480
452
  # Call the method and expect an exception
481
453
  with pytest.raises(ConnectionHandlerError, match="Failed to get table statistics"):
482
454
  await handler.get_table_stats('users')
483
-
455
+
484
456
  # Verify error was recorded
485
457
  handler.stats.record_error.assert_called_once()
486
458
 
@@ -506,21 +478,21 @@ class TestMySQLHandler:
506
478
  'referenced_column_name': 'id'
507
479
  }
508
480
  ]
509
-
481
+
510
482
  mock_cursor = mock_conn.cursor().__enter__()
511
483
  # 首先模拟表存在性检查返回成功
512
484
  mock_cursor.fetchone.side_effect = [{'count': 1}]
513
485
  mock_cursor.fetchall.return_value = constraints
514
-
486
+
515
487
  # Call the method
516
488
  result = await handler.get_table_constraints('users')
517
-
489
+
518
490
  # Verify connection was made
519
491
  mock_connect.assert_called_once()
520
-
492
+
521
493
  # Verify the cursor was used correctly
522
494
  assert mock_cursor.execute.call_count == 2 # 表存在性检查 + 约束查询
523
-
495
+
524
496
  # Verify the result format
525
497
  assert isinstance(result, str)
526
498
  assert "Constraints for users:" in result
@@ -528,7 +500,7 @@ class TestMySQLHandler:
528
500
  assert "FOREIGN KEY" in result
529
501
  assert "fk_order" in result
530
502
  assert "orders.id" in result
531
-
503
+
532
504
  # Verify connection was closed
533
505
  mock_conn.close.assert_called_once()
534
506
 
@@ -542,13 +514,13 @@ class TestMySQLHandler:
542
514
  # 首先模拟表存在性检查返回成功
543
515
  mock_cursor.fetchone.side_effect = [{'count': 1}]
544
516
  mock_cursor.fetchall.return_value = []
545
-
517
+
546
518
  # Call the method
547
519
  result = await handler.get_table_constraints('users')
548
-
520
+
549
521
  # Verify the result format
550
522
  assert result == 'No constraints found on table users'
551
-
523
+
552
524
  # Verify connection was closed
553
525
  mock_conn.close.assert_called_once()
554
526
 
@@ -560,7 +532,7 @@ class TestMySQLHandler:
560
532
  # Call the method and expect an exception
561
533
  with pytest.raises(ConnectionHandlerError, match="Failed to get constraint information"):
562
534
  await handler.get_table_constraints('users')
563
-
535
+
564
536
  # Verify error was recorded
565
537
  handler.stats.record_error.assert_called_once()
566
538
 
@@ -572,22 +544,22 @@ class TestMySQLHandler:
572
544
  # Mock cursor to return explain results
573
545
  explain_result = [{'EXPLAIN': 'Table scan on users'}]
574
546
  analyze_result = [{'EXPLAIN': 'Table scan on users (actual time=0.1..0.2 rows=100)'}]
575
-
547
+
576
548
  # Set up the mock cursor to return different data for different queries
577
549
  mock_cursor = mock_conn.cursor().__enter__()
578
550
  mock_cursor.fetchall.side_effect = [explain_result, analyze_result]
579
-
551
+
580
552
  # Call the method
581
553
  result = await handler.explain_query('SELECT * FROM users')
582
-
554
+
583
555
  # Verify connection was made
584
556
  mock_connect.assert_called_once()
585
-
557
+
586
558
  # Verify the cursor was used correctly
587
559
  assert mock_cursor.execute.call_count == 2
588
560
  mock_cursor.execute.assert_any_call('EXPLAIN FORMAT=TREE SELECT * FROM users')
589
561
  mock_cursor.execute.assert_any_call('EXPLAIN ANALYZE SELECT * FROM users')
590
-
562
+
591
563
  # Verify the result format
592
564
  assert isinstance(result, str)
593
565
  assert "Query Execution Plan:" in result
@@ -595,7 +567,7 @@ class TestMySQLHandler:
595
567
  assert "Table scan on users" in result
596
568
  assert "Actual Plan (ANALYZE):" in result
597
569
  assert "Table scan on users (actual time=0.1..0.2 rows=100)" in result
598
-
570
+
599
571
  # Verify connection was closed
600
572
  mock_conn.close.assert_called_once()
601
573
 
@@ -607,7 +579,7 @@ class TestMySQLHandler:
607
579
  # Call the method and expect an exception
608
580
  with pytest.raises(ConnectionHandlerError, match="Failed to explain query"):
609
581
  await handler.explain_query('SELECT * FROM users')
610
-
582
+
611
583
  # Verify error was recorded
612
584
  handler.stats.record_error.assert_called_once()
613
585
 
@@ -616,9 +588,134 @@ class TestMySQLHandler:
616
588
  """Test cleanup method"""
617
589
  # Mock the handler.stats.to_dict method
618
590
  handler.stats.to_dict.return_value = {'queries': 10, 'errors': 0}
619
-
591
+
620
592
  # Call the method
621
593
  await handler.cleanup()
622
-
594
+
623
595
  # Verify log was called
624
- handler.log.assert_called_once_with('info', 'Final MySQL handler stats: {\'queries\': 10, \'errors\': 0}')
596
+ handler.log.assert_called_once_with('info', 'Final MySQL handler stats: {\'queries\': 10, \'errors\': 0}')
597
+
598
+ @pytest.mark.asyncio
599
+ async def test_special_character_password(self):
600
+ """Test handling of special characters in password"""
601
+ # Create a handler with a password containing special characters
602
+ with patch('os.path.exists', return_value=True), \
603
+ patch('builtins.open', MagicMock()), \
604
+ patch('yaml.safe_load', return_value={
605
+ 'connections': {
606
+ 'test_mysql': {
607
+ 'type': 'mysql',
608
+ 'host': 'localhost',
609
+ 'port': 3306,
610
+ 'user': 'testuser',
611
+ 'password': 'test?pass!@#$%^&*()', # Password with special characters
612
+ 'database': 'testdb'
613
+ }
614
+ }
615
+ }):
616
+ special_handler = MySQLHandler('config.yaml', 'test_mysql')
617
+ special_handler.log = MagicMock()
618
+ special_handler.stats = MagicMock()
619
+
620
+ # Mock successful connection
621
+ mock_conn = MagicMock()
622
+ mock_cursor = MagicMock()
623
+ mock_cursor.__enter__.return_value = mock_cursor
624
+ mock_cursor.fetchall.return_value = [
625
+ {'table_name': 'users', 'description': 'User table'}
626
+ ]
627
+ mock_conn.cursor.return_value = mock_cursor
628
+
629
+ with patch('mysql.connector.connect', return_value=mock_conn) as mock_connect:
630
+ # Test get_tables method
631
+ result = await special_handler.get_tables()
632
+
633
+ # Verify connection was made with correct parameters including special character password
634
+ mock_connect.assert_called_once()
635
+ call_args = mock_connect.call_args[1]
636
+ assert 'password' in call_args
637
+ assert call_args['password'] == 'test?pass!@#$%^&*()'
638
+
639
+ # Verify the result
640
+ assert len(result) == 1
641
+ assert result[0].name == 'users schema'
642
+
643
+ # Verify connection was closed
644
+ mock_conn.close.assert_called_once()
645
+
646
+ @pytest.mark.asyncio
647
+ async def test_special_character_password_connection_error(self):
648
+ """Test error handling with special characters in password"""
649
+ # Create a handler with a password containing special characters
650
+ with patch('os.path.exists', return_value=True), \
651
+ patch('builtins.open', MagicMock()), \
652
+ patch('yaml.safe_load', return_value={
653
+ 'connections': {
654
+ 'test_mysql': {
655
+ 'type': 'mysql',
656
+ 'host': 'localhost',
657
+ 'port': 3306,
658
+ 'user': 'testuser',
659
+ 'password': 'test?pass!@#$%^&*()', # Password with special characters
660
+ 'database': 'testdb'
661
+ }
662
+ }
663
+ }):
664
+ special_handler = MySQLHandler('config.yaml', 'test_mysql')
665
+ special_handler.log = MagicMock()
666
+ special_handler.stats = MagicMock()
667
+
668
+ # Mock connection failure
669
+ with patch('mysql.connector.connect', side_effect=mysql.connector.Error('Connection failed')):
670
+ # Test get_tables method with connection failure
671
+ with pytest.raises(ConnectionHandlerError, match="Failed to get tables"):
672
+ await special_handler.get_tables()
673
+
674
+ # Verify error was recorded
675
+ special_handler.stats.record_error.assert_called_once()
676
+
677
+ @pytest.mark.asyncio
678
+ async def test_variable_scope_handling(self):
679
+ """Test proper handling of variable scope in connection methods"""
680
+ # Create a handler
681
+ with patch('os.path.exists', return_value=True), \
682
+ patch('builtins.open', MagicMock()), \
683
+ patch('yaml.safe_load', return_value={
684
+ 'connections': {
685
+ 'test_mysql': {
686
+ 'type': 'mysql',
687
+ 'host': 'localhost',
688
+ 'port': 3306,
689
+ 'user': 'testuser',
690
+ 'password': 'testpass',
691
+ 'database': 'testdb'
692
+ }
693
+ }
694
+ }):
695
+ handler = MySQLHandler('config.yaml', 'test_mysql')
696
+ handler.log = MagicMock()
697
+ handler.stats = MagicMock()
698
+
699
+ # Test all methods that use connection handling with try/finally blocks
700
+ methods_to_test = [
701
+ ('get_tables', []),
702
+ ('get_schema', ['users']),
703
+ ('get_table_description', ['users']),
704
+ ('get_table_ddl', ['users']),
705
+ ('get_table_indexes', ['users']),
706
+ ('get_table_stats', ['users']),
707
+ ('get_table_constraints', ['users']),
708
+ ('explain_query', ['SELECT * FROM users'])
709
+ ]
710
+
711
+ for method_name, args in methods_to_test:
712
+ # Mock connection failure
713
+ with patch('mysql.connector.connect', side_effect=mysql.connector.Error('Connection failed')):
714
+ method = getattr(handler, method_name)
715
+
716
+ # Call the method and expect an exception
717
+ with pytest.raises(ConnectionHandlerError):
718
+ await method(*args)
719
+
720
+ # Verify error was recorded
721
+ handler.stats.record_error.assert_called()