mcp-dbutils 1.0.0__tar.gz → 1.0.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.
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/CHANGELOG.md +7 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/PKG-INFO +1 -1
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/pyproject.toml +1 -1
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/src/mcp_dbutils/base.py +7 -12
- mcp_dbutils-1.0.1/tests/unit/test_base_extended.py +504 -0
- mcp_dbutils-1.0.1/tests/unit/test_init.py +141 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/unit/test_write_permissions.py +12 -11
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.coveragerc +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/bug_report_ar.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/bug_report_en.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/bug_report_es.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/bug_report_fr.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/bug_report_ru.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/bug_report_zh.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/documentation_improvement_ar.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/documentation_improvement_en.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/documentation_improvement_es.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/documentation_improvement_fr.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/documentation_improvement_ru.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/documentation_improvement_zh.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/feature_request_ar.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/feature_request_en.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/feature_request_es.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/feature_request_fr.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/feature_request_ru.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/feature_request_zh.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/performance_issue_ar.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/performance_issue_en.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/performance_issue_es.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/performance_issue_fr.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/performance_issue_ru.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/performance_issue_zh.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/security_vulnerability_ar.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/security_vulnerability_en.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/security_vulnerability_es.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/security_vulnerability_fr.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/security_vulnerability_ru.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/ISSUE_TEMPLATE/security_vulnerability_zh.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/workflows/code-style.yml +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/workflows/issue-translator.yml +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/workflows/quality-assurance.yml +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.github/workflows/release.yml +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.gitignore +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.pre-commit-config.yaml +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/.releaserc.json +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/Dockerfile +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/LICENSE +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/README.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/README_AR.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/README_EN.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/README_ES.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/README_FR.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/README_RU.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/config.yaml.example +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/coverage.xml +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ar/configuration.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ar/examples/README.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ar/examples/advanced-llm-interactions.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ar/examples/mysql-examples.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ar/examples/postgresql-examples.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ar/examples/sqlite-examples.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ar/installation-platform-specific.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ar/installation.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ar/technical/architecture.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ar/technical/development.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ar/technical/security.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ar/technical/sonarcloud-integration.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ar/technical/testing.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ar/usage.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/document-consistency-check.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/document-version-history.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/en/configuration.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/en/examples/advanced-llm-interactions.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/en/examples/mysql-examples.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/en/examples/postgresql-examples.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/en/examples/sqlite-examples.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/en/installation-platform-specific.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/en/installation.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/en/technical/architecture.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/en/technical/development.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/en/technical/security.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/en/technical/sonarcloud-integration.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/en/technical/testing.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/en/usage.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/es/configuration.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/es/examples/README.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/es/examples/advanced-llm-interactions.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/es/examples/mysql-examples.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/es/examples/postgresql-examples.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/es/examples/sqlite-examples.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/es/installation-platform-specific.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/es/installation.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/es/technical/architecture.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/es/technical/development.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/es/technical/security.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/es/technical/sonarcloud-integration.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/es/technical/testing.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/es/usage.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/fr/configuration.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/fr/examples/README.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/fr/examples/advanced-llm-interactions.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/fr/examples/mysql-examples.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/fr/examples/postgresql-examples.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/fr/examples/sqlite-examples.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/fr/installation-platform-specific.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/fr/installation.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/fr/technical/architecture.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/fr/technical/development.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/fr/technical/security.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/fr/technical/sonarcloud-integration.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/fr/technical/testing.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/fr/usage.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/index.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ru/configuration.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ru/examples/README.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ru/examples/advanced-llm-interactions.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ru/examples/mysql-examples.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ru/examples/postgresql-examples.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ru/examples/sqlite-examples.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ru/installation-platform-specific.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ru/installation.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ru/technical/architecture.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ru/technical/development.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ru/technical/security.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ru/technical/sonarcloud-integration.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ru/technical/testing.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/ru/usage.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/write-operations.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/zh/configuration-write-operations.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/zh/configuration.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/zh/examples/advanced-llm-interactions.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/zh/examples/mysql-examples.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/zh/examples/postgresql-examples.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/zh/examples/sqlite-examples.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/zh/installation-platform-specific.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/zh/installation.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/zh/security-best-practices.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/zh/technical/architecture.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/zh/technical/audit-logging.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/zh/technical/development.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/zh/technical/security.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/zh/technical/sonarcloud-integration.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/zh/technical/testing.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/zh/technical/write-operations-design.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/zh/usage-write-operations.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/docs/zh/usage.md +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/examples/config.yaml +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/scripts/check_docs_consistency.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/scripts/check_zh_docs.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/scripts/fix_en_nav_links.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/scripts/fix_imports.sh +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/scripts/fix_remaining_issues.sh +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/scripts/fix_zh_nav_links.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/scripts/run_sonar_analysis.sh +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/scripts/sonar-ai-fix.fish +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/smithery.yaml +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/sonar-project.properties +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/src/mcp_dbutils/__init__.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/src/mcp_dbutils/audit.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/src/mcp_dbutils/config.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/src/mcp_dbutils/log.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/src/mcp_dbutils/mysql/__init__.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/src/mcp_dbutils/mysql/config.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/src/mcp_dbutils/mysql/handler.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/src/mcp_dbutils/mysql/server.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/src/mcp_dbutils/postgres/__init__.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/src/mcp_dbutils/postgres/config.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/src/mcp_dbutils/postgres/handler.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/src/mcp_dbutils/postgres/server.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/src/mcp_dbutils/sqlite/__init__.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/src/mcp_dbutils/sqlite/config.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/src/mcp_dbutils/sqlite/handler.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/src/mcp_dbutils/sqlite/server.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/src/mcp_dbutils/stats.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/conftest.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/__init__.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/conftest.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/fixtures.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/test_list_connections.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/test_logging.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/test_monitoring.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/test_monitoring_enhanced.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/test_mysql.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/test_mysql_config.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/test_mysql_config_helpers.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/test_mysql_handler_extended.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/test_postgres.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/test_postgres_config.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/test_prompts.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/test_sqlite.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/test_sqlite_config.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/test_sqlite_handler_extended.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/test_tools.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/integration/test_tools_advanced.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/test_write_operations.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/unit/test_audit.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/unit/test_base.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/unit/test_base_handlers.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/unit/test_base_helpers.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/unit/test_base_server.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/unit/test_base_write_operations.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/unit/test_log.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/unit/test_mysql_handler.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/unit/test_mysql_server.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/unit/test_postgres_handler.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/unit/test_postgres_server.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/unit/test_sql_parsing.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/unit/test_sqlite_handler.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/unit/test_sqlite_server.py +0 -0
- {mcp_dbutils-1.0.0 → mcp_dbutils-1.0.1}/tests/unit/test_stats.py +0 -0
@@ -1,3 +1,10 @@
|
|
1
|
+
## [1.0.1](https://github.com/donghao1393/mcp-dbutils/compare/v1.0.0...v1.0.1) (2025-05-04)
|
2
|
+
|
3
|
+
|
4
|
+
### Bug Fixes
|
5
|
+
|
6
|
+
* 提高代码覆盖率以解决SonarCloud检查失败问题 ([#90](https://github.com/donghao1393/mcp-dbutils/issues/90)) ([adbc9f2](https://github.com/donghao1393/mcp-dbutils/commit/adbc9f2e6fb54ed446d96775c65edf67611637ed))
|
7
|
+
|
1
8
|
# [1.0.0](https://github.com/donghao1393/mcp-dbutils/compare/v0.23.1...v1.0.0) (2025-05-04)
|
2
9
|
|
3
10
|
|
@@ -188,8 +188,8 @@ class ConnectionHandler(ABC):
|
|
188
188
|
if "row" in result and "affected" in result:
|
189
189
|
# 从结果字符串中提取受影响的行数
|
190
190
|
import re
|
191
|
-
#
|
192
|
-
match = re.search(r"(\d
|
191
|
+
# 限制数字长度,避免DoS风险
|
192
|
+
match = re.search(r"(\d{1,10}) rows?", result)
|
193
193
|
if match:
|
194
194
|
affected_rows = int(match.group(1))
|
195
195
|
except Exception:
|
@@ -601,7 +601,7 @@ class ConnectionServer:
|
|
601
601
|
# Default fallback
|
602
602
|
return "unknown_table"
|
603
603
|
|
604
|
-
async def _check_write_permission(self, connection: str, table_name: str, operation_type: str) ->
|
604
|
+
async def _check_write_permission(self, connection: str, table_name: str, operation_type: str) -> None:
|
605
605
|
"""检查写操作权限
|
606
606
|
|
607
607
|
Args:
|
@@ -609,9 +609,6 @@ class ConnectionServer:
|
|
609
609
|
table_name: 表名
|
610
610
|
operation_type: 操作类型 (INSERT, UPDATE, DELETE)
|
611
611
|
|
612
|
-
Returns:
|
613
|
-
bool: 是否有权限执行写操作
|
614
|
-
|
615
612
|
Raises:
|
616
613
|
ConfigurationError: 如果连接不可写或没有表级权限
|
617
614
|
"""
|
@@ -626,7 +623,7 @@ class ConnectionServer:
|
|
626
623
|
write_permissions = db_config.get("write_permissions", {})
|
627
624
|
if not write_permissions:
|
628
625
|
# 没有细粒度权限控制,默认允许所有写操作
|
629
|
-
return
|
626
|
+
return
|
630
627
|
|
631
628
|
# 检查表级权限
|
632
629
|
tables = write_permissions.get("tables", {})
|
@@ -634,7 +631,7 @@ class ConnectionServer:
|
|
634
631
|
# 没有表级权限配置,检查默认策略
|
635
632
|
default_policy = write_permissions.get("default_policy", "read_only")
|
636
633
|
if default_policy == "allow_all":
|
637
|
-
return
|
634
|
+
return
|
638
635
|
else:
|
639
636
|
# 默认只读
|
640
637
|
raise ConfigurationError(WRITE_OPERATION_NOT_ALLOWED_ERROR.format(
|
@@ -646,7 +643,7 @@ class ConnectionServer:
|
|
646
643
|
table_config = tables[table_name]
|
647
644
|
operations = table_config.get("operations", ["INSERT", "UPDATE", "DELETE"])
|
648
645
|
if operation_type in operations:
|
649
|
-
return
|
646
|
+
return
|
650
647
|
else:
|
651
648
|
raise ConfigurationError(WRITE_OPERATION_NOT_ALLOWED_ERROR.format(
|
652
649
|
operation=operation_type, table=table_name
|
@@ -655,15 +652,13 @@ class ConnectionServer:
|
|
655
652
|
# 表未明确配置,检查默认策略
|
656
653
|
default_policy = write_permissions.get("default_policy", "read_only")
|
657
654
|
if default_policy == "allow_all":
|
658
|
-
return
|
655
|
+
return
|
659
656
|
else:
|
660
657
|
# 默认只读
|
661
658
|
raise ConfigurationError(WRITE_OPERATION_NOT_ALLOWED_ERROR.format(
|
662
659
|
operation=operation_type, table=table_name
|
663
660
|
))
|
664
661
|
|
665
|
-
return False
|
666
|
-
|
667
662
|
def _create_handler_for_type(
|
668
663
|
self, db_type: str, connection: str
|
669
664
|
) -> ConnectionHandler:
|
@@ -0,0 +1,504 @@
|
|
1
|
+
"""Extended tests for base module"""
|
2
|
+
import os
|
3
|
+
import tempfile
|
4
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
5
|
+
|
6
|
+
import mcp.types as types
|
7
|
+
import pytest
|
8
|
+
import yaml
|
9
|
+
|
10
|
+
from mcp_dbutils.base import (
|
11
|
+
CONNECTION_NOT_WRITABLE_ERROR,
|
12
|
+
UNSUPPORTED_WRITE_OPERATION_ERROR,
|
13
|
+
WRITE_CONFIRMATION_REQUIRED_ERROR,
|
14
|
+
WRITE_OPERATION_NOT_ALLOWED_ERROR,
|
15
|
+
ConfigurationError,
|
16
|
+
ConnectionHandler,
|
17
|
+
ConnectionServer,
|
18
|
+
)
|
19
|
+
|
20
|
+
|
21
|
+
class TestConnectionServerExtended:
|
22
|
+
"""Extended tests for ConnectionServer class"""
|
23
|
+
|
24
|
+
@pytest.fixture
|
25
|
+
def config_file(self):
|
26
|
+
"""Create a temporary config file for testing"""
|
27
|
+
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".yaml") as f:
|
28
|
+
config = {
|
29
|
+
"connections": {
|
30
|
+
"test_conn": {
|
31
|
+
"type": "sqlite",
|
32
|
+
"database": ":memory:",
|
33
|
+
"writable": True
|
34
|
+
},
|
35
|
+
"test_conn_readonly": {
|
36
|
+
"type": "sqlite",
|
37
|
+
"database": ":memory:",
|
38
|
+
"writable": False
|
39
|
+
},
|
40
|
+
"test_conn_with_permissions": {
|
41
|
+
"type": "sqlite",
|
42
|
+
"database": ":memory:",
|
43
|
+
"writable": True,
|
44
|
+
"write_permissions": {
|
45
|
+
"tables": {
|
46
|
+
"users": {
|
47
|
+
"operations": ["INSERT", "UPDATE"]
|
48
|
+
},
|
49
|
+
"posts": {
|
50
|
+
"operations": ["INSERT", "UPDATE", "DELETE"]
|
51
|
+
}
|
52
|
+
},
|
53
|
+
"default_policy": "read_only"
|
54
|
+
}
|
55
|
+
},
|
56
|
+
"test_conn_allow_all": {
|
57
|
+
"type": "sqlite",
|
58
|
+
"database": ":memory:",
|
59
|
+
"writable": True,
|
60
|
+
"write_permissions": {
|
61
|
+
"default_policy": "allow_all"
|
62
|
+
}
|
63
|
+
}
|
64
|
+
}
|
65
|
+
}
|
66
|
+
yaml.dump(config, f)
|
67
|
+
f.flush()
|
68
|
+
yield f.name
|
69
|
+
os.unlink(f.name)
|
70
|
+
|
71
|
+
@pytest.fixture
|
72
|
+
def server(self, config_file):
|
73
|
+
"""Create a ConnectionServer instance for testing"""
|
74
|
+
server = ConnectionServer(config_file)
|
75
|
+
server.send_log = MagicMock()
|
76
|
+
return server
|
77
|
+
|
78
|
+
@pytest.mark.asyncio
|
79
|
+
async def test_check_write_permission_writable(self, server):
|
80
|
+
"""Test _check_write_permission with writable connection"""
|
81
|
+
# Connection is writable and has no specific permissions
|
82
|
+
# Method should return None (no exception) if permission is granted
|
83
|
+
await server._check_write_permission("test_conn", "users", "INSERT")
|
84
|
+
await server._check_write_permission("test_conn", "users", "UPDATE")
|
85
|
+
await server._check_write_permission("test_conn", "users", "DELETE")
|
86
|
+
|
87
|
+
@pytest.mark.asyncio
|
88
|
+
async def test_check_write_permission_readonly(self, server):
|
89
|
+
"""Test _check_write_permission with readonly connection"""
|
90
|
+
# Connection is not writable
|
91
|
+
with pytest.raises(ConfigurationError, match=CONNECTION_NOT_WRITABLE_ERROR):
|
92
|
+
await server._check_write_permission("test_conn_readonly", "users", "INSERT")
|
93
|
+
|
94
|
+
@pytest.mark.asyncio
|
95
|
+
async def test_check_write_permission_with_table_permissions(self, server):
|
96
|
+
"""Test _check_write_permission with table-specific permissions"""
|
97
|
+
# Table 'users' allows INSERT and UPDATE but not DELETE
|
98
|
+
await server._check_write_permission("test_conn_with_permissions", "users", "INSERT")
|
99
|
+
await server._check_write_permission("test_conn_with_permissions", "users", "UPDATE")
|
100
|
+
|
101
|
+
with pytest.raises(ConfigurationError) as excinfo:
|
102
|
+
await server._check_write_permission("test_conn_with_permissions", "users", "DELETE")
|
103
|
+
assert "No permission to perform DELETE operation on table users" in str(excinfo.value)
|
104
|
+
|
105
|
+
# Table 'posts' allows all operations
|
106
|
+
await server._check_write_permission("test_conn_with_permissions", "posts", "INSERT")
|
107
|
+
await server._check_write_permission("test_conn_with_permissions", "posts", "UPDATE")
|
108
|
+
await server._check_write_permission("test_conn_with_permissions", "posts", "DELETE")
|
109
|
+
|
110
|
+
# Table 'unknown_table' is not explicitly configured, default policy is read_only
|
111
|
+
with pytest.raises(ConfigurationError) as excinfo:
|
112
|
+
await server._check_write_permission("test_conn_with_permissions", "unknown_table", "INSERT")
|
113
|
+
assert "No permission to perform INSERT operation on table unknown_table" in str(excinfo.value)
|
114
|
+
|
115
|
+
@pytest.mark.asyncio
|
116
|
+
async def test_check_write_permission_allow_all(self, server):
|
117
|
+
"""Test _check_write_permission with allow_all default policy"""
|
118
|
+
# Default policy is allow_all
|
119
|
+
await server._check_write_permission("test_conn_allow_all", "any_table", "INSERT")
|
120
|
+
await server._check_write_permission("test_conn_allow_all", "any_table", "UPDATE")
|
121
|
+
await server._check_write_permission("test_conn_allow_all", "any_table", "DELETE")
|
122
|
+
|
123
|
+
@pytest.mark.asyncio
|
124
|
+
async def test_handle_execute_write_success(self, server):
|
125
|
+
"""Test _handle_execute_write with successful operation"""
|
126
|
+
# Mock the handler
|
127
|
+
mock_handler = AsyncMock()
|
128
|
+
mock_handler.execute_write_query.return_value = "Write operation executed successfully. 1 row affected."
|
129
|
+
|
130
|
+
# Mock the get_handler method to return an async context manager
|
131
|
+
class AsyncContextManagerMock:
|
132
|
+
async def __aenter__(self):
|
133
|
+
return mock_handler
|
134
|
+
|
135
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
136
|
+
return None
|
137
|
+
|
138
|
+
server.get_handler = MagicMock(return_value=AsyncContextManagerMock())
|
139
|
+
|
140
|
+
# Mock the _check_write_permission method
|
141
|
+
server._check_write_permission = AsyncMock()
|
142
|
+
|
143
|
+
result = await server._handle_execute_write(
|
144
|
+
connection="test_conn",
|
145
|
+
sql="INSERT INTO users (name) VALUES ('test')",
|
146
|
+
confirmation="CONFIRM_WRITE"
|
147
|
+
)
|
148
|
+
|
149
|
+
# Verify handler was called
|
150
|
+
server.get_handler.assert_called_once_with("test_conn")
|
151
|
+
server._check_write_permission.assert_called_once_with("test_conn", "USERS", "INSERT")
|
152
|
+
mock_handler.execute_write_query.assert_called_once_with("INSERT INTO users (name) VALUES ('test')")
|
153
|
+
|
154
|
+
# Verify result
|
155
|
+
assert isinstance(result, list)
|
156
|
+
assert len(result) == 1
|
157
|
+
assert result[0].type == "text"
|
158
|
+
assert "Write operation executed successfully" in result[0].text
|
159
|
+
|
160
|
+
@pytest.mark.asyncio
|
161
|
+
async def test_handle_execute_write_no_confirmation(self, server):
|
162
|
+
"""Test _handle_execute_write without confirmation"""
|
163
|
+
with pytest.raises(ConfigurationError, match=WRITE_CONFIRMATION_REQUIRED_ERROR):
|
164
|
+
await server._handle_execute_write(
|
165
|
+
connection="test_conn",
|
166
|
+
sql="INSERT INTO users (name) VALUES ('test')",
|
167
|
+
confirmation=""
|
168
|
+
)
|
169
|
+
|
170
|
+
@pytest.mark.asyncio
|
171
|
+
async def test_handle_execute_write_invalid_confirmation(self, server):
|
172
|
+
"""Test _handle_execute_write with invalid confirmation"""
|
173
|
+
with pytest.raises(ConfigurationError, match=WRITE_CONFIRMATION_REQUIRED_ERROR):
|
174
|
+
await server._handle_execute_write(
|
175
|
+
connection="test_conn",
|
176
|
+
sql="INSERT INTO users (name) VALUES ('test')",
|
177
|
+
confirmation="WRONG_CONFIRMATION"
|
178
|
+
)
|
179
|
+
|
180
|
+
@pytest.mark.asyncio
|
181
|
+
async def test_handle_execute_write_unsupported_operation(self, server):
|
182
|
+
"""Test _handle_execute_write with unsupported operation"""
|
183
|
+
with pytest.raises(ConfigurationError, match=UNSUPPORTED_WRITE_OPERATION_ERROR.format(operation="SELECT")):
|
184
|
+
await server._handle_execute_write(
|
185
|
+
connection="test_conn",
|
186
|
+
sql="SELECT * FROM users",
|
187
|
+
confirmation="CONFIRM_WRITE"
|
188
|
+
)
|
189
|
+
|
190
|
+
@pytest.mark.asyncio
|
191
|
+
async def test_handle_execute_write_no_permission(self, server):
|
192
|
+
"""Test _handle_execute_write without permission"""
|
193
|
+
# Mock the _check_write_permission method to raise error
|
194
|
+
server._check_write_permission = AsyncMock(
|
195
|
+
side_effect=ConfigurationError(WRITE_OPERATION_NOT_ALLOWED_ERROR.format(
|
196
|
+
operation="DELETE", table="users"
|
197
|
+
))
|
198
|
+
)
|
199
|
+
|
200
|
+
with pytest.raises(ConfigurationError, match="No permission to perform DELETE operation on table users"):
|
201
|
+
await server._handle_execute_write(
|
202
|
+
connection="test_conn_with_permissions",
|
203
|
+
sql="DELETE FROM users WHERE id = 1",
|
204
|
+
confirmation="CONFIRM_WRITE"
|
205
|
+
)
|
206
|
+
|
207
|
+
@pytest.mark.asyncio
|
208
|
+
async def test_handle_execute_write_execution_error(self, server):
|
209
|
+
"""Test _handle_execute_write with execution error"""
|
210
|
+
# Mock the handler
|
211
|
+
mock_handler = AsyncMock()
|
212
|
+
mock_handler.execute_write_query.side_effect = Exception("Execution error")
|
213
|
+
|
214
|
+
# Mock the get_handler method to return an async context manager
|
215
|
+
class AsyncContextManagerMock:
|
216
|
+
async def __aenter__(self):
|
217
|
+
return mock_handler
|
218
|
+
|
219
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
220
|
+
return None
|
221
|
+
|
222
|
+
server.get_handler = MagicMock(return_value=AsyncContextManagerMock())
|
223
|
+
|
224
|
+
# Mock the _check_write_permission method
|
225
|
+
server._check_write_permission = AsyncMock()
|
226
|
+
|
227
|
+
with pytest.raises(Exception, match="Execution error"):
|
228
|
+
await server._handle_execute_write(
|
229
|
+
connection="test_conn",
|
230
|
+
sql="INSERT INTO users (name) VALUES ('test')",
|
231
|
+
confirmation="CONFIRM_WRITE"
|
232
|
+
)
|
233
|
+
|
234
|
+
# No need to verify result as we're expecting an exception
|
235
|
+
|
236
|
+
def test_get_sql_type(self, server):
|
237
|
+
"""Test _get_sql_type method"""
|
238
|
+
# Test SELECT statement
|
239
|
+
assert server._get_sql_type("SELECT * FROM users") == "SELECT"
|
240
|
+
assert server._get_sql_type(" SELECT * FROM users") == "SELECT"
|
241
|
+
assert server._get_sql_type("select * from users") == "SELECT"
|
242
|
+
|
243
|
+
# Test INSERT statement
|
244
|
+
assert server._get_sql_type("INSERT INTO users VALUES (1, 'test')") == "INSERT"
|
245
|
+
assert server._get_sql_type("insert into users values (1, 'test')") == "INSERT"
|
246
|
+
|
247
|
+
# Test UPDATE statement
|
248
|
+
assert server._get_sql_type("UPDATE users SET name = 'test' WHERE id = 1") == "UPDATE"
|
249
|
+
assert server._get_sql_type("update users set name = 'test' where id = 1") == "UPDATE"
|
250
|
+
|
251
|
+
# Test DELETE statement
|
252
|
+
assert server._get_sql_type("DELETE FROM users WHERE id = 1") == "DELETE"
|
253
|
+
assert server._get_sql_type("delete from users where id = 1") == "DELETE"
|
254
|
+
|
255
|
+
# Test CREATE statement
|
256
|
+
assert server._get_sql_type("CREATE TABLE users (id INT, name TEXT)") == "CREATE"
|
257
|
+
assert server._get_sql_type("create table users (id int, name text)") == "CREATE"
|
258
|
+
|
259
|
+
# Test ALTER statement
|
260
|
+
assert server._get_sql_type("ALTER TABLE users ADD COLUMN email TEXT") == "ALTER"
|
261
|
+
assert server._get_sql_type("alter table users add column email text") == "ALTER"
|
262
|
+
|
263
|
+
# Test DROP statement
|
264
|
+
assert server._get_sql_type("DROP TABLE users") == "DROP"
|
265
|
+
assert server._get_sql_type("drop table users") == "DROP"
|
266
|
+
|
267
|
+
# Test TRUNCATE statement
|
268
|
+
assert server._get_sql_type("TRUNCATE TABLE users") == "TRUNCATE"
|
269
|
+
assert server._get_sql_type("truncate table users") == "TRUNCATE"
|
270
|
+
|
271
|
+
# Test transaction statements
|
272
|
+
assert server._get_sql_type("BEGIN TRANSACTION") == "TRANSACTION_START"
|
273
|
+
assert server._get_sql_type("START TRANSACTION") == "TRANSACTION_START"
|
274
|
+
assert server._get_sql_type("COMMIT") == "TRANSACTION_COMMIT"
|
275
|
+
assert server._get_sql_type("ROLLBACK") == "TRANSACTION_ROLLBACK"
|
276
|
+
|
277
|
+
# Test unknown statement
|
278
|
+
assert server._get_sql_type("UNKNOWN STATEMENT") == "UNKNOWN"
|
279
|
+
assert server._get_sql_type("") == "UNKNOWN"
|
280
|
+
|
281
|
+
def test_extract_table_name(self, server):
|
282
|
+
"""Test _extract_table_name method"""
|
283
|
+
# Test INSERT statement
|
284
|
+
assert server._extract_table_name("INSERT INTO users VALUES (1, 'test')").lower() == "users"
|
285
|
+
assert server._extract_table_name("INSERT INTO public.users VALUES (1, 'test')").lower() == "public.users"
|
286
|
+
|
287
|
+
# Test UPDATE statement
|
288
|
+
assert server._extract_table_name("UPDATE users SET name = 'test' WHERE id = 1").lower() == "users"
|
289
|
+
assert server._extract_table_name("UPDATE public.users SET name = 'test' WHERE id = 1").lower() == "public.users"
|
290
|
+
|
291
|
+
# Test DELETE statement
|
292
|
+
assert server._extract_table_name("DELETE FROM users WHERE id = 1").lower() == "users"
|
293
|
+
assert server._extract_table_name("DELETE FROM public.users WHERE id = 1").lower() == "public.users"
|
294
|
+
|
295
|
+
# Test with quoted table name
|
296
|
+
assert server._extract_table_name('INSERT INTO "users" VALUES (1, \'test\')').lower() == "users"
|
297
|
+
assert server._extract_table_name("INSERT INTO `users` VALUES (1, 'test')").lower() == "users"
|
298
|
+
assert server._extract_table_name("INSERT INTO [users] VALUES (1, 'test')").lower() == "users"
|
299
|
+
|
300
|
+
# Test unknown statement
|
301
|
+
assert server._extract_table_name("UNKNOWN STATEMENT") == "unknown_table"
|
302
|
+
|
303
|
+
|
304
|
+
class TestConnectionHandlerExtended:
|
305
|
+
"""Extended tests for ConnectionHandler class"""
|
306
|
+
|
307
|
+
class MockHandler(ConnectionHandler):
|
308
|
+
"""Mock implementation of ConnectionHandler for testing"""
|
309
|
+
|
310
|
+
@property
|
311
|
+
def db_type(self) -> str:
|
312
|
+
return "mock"
|
313
|
+
|
314
|
+
async def get_tables(self) -> list[types.Resource]:
|
315
|
+
return []
|
316
|
+
|
317
|
+
async def get_schema(self, table_name: str) -> str:
|
318
|
+
return "{}"
|
319
|
+
|
320
|
+
async def _execute_query(self, sql: str) -> str:
|
321
|
+
return "Query executed"
|
322
|
+
|
323
|
+
async def _execute_write_query(self, sql: str) -> str:
|
324
|
+
return "Write operation executed successfully. 1 row affected."
|
325
|
+
|
326
|
+
async def get_table_description(self, table_name: str) -> str:
|
327
|
+
return f"Description of {table_name}"
|
328
|
+
|
329
|
+
async def get_table_ddl(self, table_name: str) -> str:
|
330
|
+
return f"DDL for {table_name}"
|
331
|
+
|
332
|
+
async def get_table_indexes(self, table_name: str) -> str:
|
333
|
+
return f"Indexes for {table_name}"
|
334
|
+
|
335
|
+
async def get_table_stats(self, table_name: str) -> str:
|
336
|
+
return f"Stats for {table_name}"
|
337
|
+
|
338
|
+
async def get_table_constraints(self, table_name: str) -> str:
|
339
|
+
return f"Constraints for {table_name}"
|
340
|
+
|
341
|
+
async def explain_query(self, sql: str) -> str:
|
342
|
+
return f"Explanation for {sql}"
|
343
|
+
|
344
|
+
async def test_connection(self) -> bool:
|
345
|
+
return True
|
346
|
+
|
347
|
+
async def cleanup(self):
|
348
|
+
pass
|
349
|
+
|
350
|
+
@pytest.fixture
|
351
|
+
def config_file(self):
|
352
|
+
"""Create a temporary config file for testing"""
|
353
|
+
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".yaml") as f:
|
354
|
+
config = {
|
355
|
+
"connections": {
|
356
|
+
"test_conn": {
|
357
|
+
"type": "mock",
|
358
|
+
"database": ":memory:"
|
359
|
+
}
|
360
|
+
}
|
361
|
+
}
|
362
|
+
yaml.dump(config, f)
|
363
|
+
f.flush()
|
364
|
+
yield f.name
|
365
|
+
os.unlink(f.name)
|
366
|
+
|
367
|
+
@pytest.fixture
|
368
|
+
def handler(self, config_file):
|
369
|
+
"""Create a MockHandler instance for testing"""
|
370
|
+
handler = self.MockHandler(config_file, "test_conn")
|
371
|
+
handler.send_log = MagicMock()
|
372
|
+
handler._session = MagicMock()
|
373
|
+
return handler
|
374
|
+
|
375
|
+
@pytest.mark.asyncio
|
376
|
+
async def test_execute_query(self, handler):
|
377
|
+
"""Test execute_query method"""
|
378
|
+
# Mock the _execute_query method
|
379
|
+
handler._execute_query = AsyncMock(return_value="Query result")
|
380
|
+
|
381
|
+
result = await handler.execute_query("SELECT * FROM users")
|
382
|
+
|
383
|
+
# Verify _execute_query was called
|
384
|
+
handler._execute_query.assert_called_once_with("SELECT * FROM users")
|
385
|
+
|
386
|
+
# Verify result
|
387
|
+
assert result == "Query result"
|
388
|
+
|
389
|
+
# Verify stats were updated
|
390
|
+
assert handler.stats.query_count == 1
|
391
|
+
assert len(handler.stats.query_durations) == 1
|
392
|
+
|
393
|
+
@pytest.mark.asyncio
|
394
|
+
async def test_execute_query_error(self, handler):
|
395
|
+
"""Test execute_query method with error"""
|
396
|
+
# Mock the _execute_query method to raise error
|
397
|
+
handler._execute_query = AsyncMock(side_effect=Exception("Query error"))
|
398
|
+
|
399
|
+
with pytest.raises(Exception, match="Query error"):
|
400
|
+
await handler.execute_query("SELECT * FROM users")
|
401
|
+
|
402
|
+
# Verify stats were updated
|
403
|
+
assert handler.stats.query_count == 1
|
404
|
+
assert handler.stats.error_count == 1
|
405
|
+
assert "Exception" in handler.stats.error_types
|
406
|
+
|
407
|
+
@pytest.mark.asyncio
|
408
|
+
async def test_execute_write_query(self, handler):
|
409
|
+
"""Test execute_write_query method"""
|
410
|
+
# Mock the _execute_write_query method
|
411
|
+
handler._execute_write_query = AsyncMock(return_value="Write operation executed successfully. 1 row affected.")
|
412
|
+
|
413
|
+
result = await handler.execute_write_query("INSERT INTO users (name) VALUES ('test')")
|
414
|
+
|
415
|
+
# Verify _execute_write_query was called
|
416
|
+
handler._execute_write_query.assert_called_once_with("INSERT INTO users (name) VALUES ('test')")
|
417
|
+
|
418
|
+
# Verify result
|
419
|
+
assert result == "Write operation executed successfully. 1 row affected."
|
420
|
+
|
421
|
+
# Verify stats were updated
|
422
|
+
assert handler.stats.query_count == 1
|
423
|
+
assert len(handler.stats.query_durations) == 1
|
424
|
+
|
425
|
+
@pytest.mark.asyncio
|
426
|
+
async def test_execute_write_query_error(self, handler):
|
427
|
+
"""Test execute_write_query method with error"""
|
428
|
+
# Mock the _execute_write_query method to raise error
|
429
|
+
handler._execute_write_query = AsyncMock(side_effect=Exception("Write error"))
|
430
|
+
|
431
|
+
with pytest.raises(Exception, match="Write error"):
|
432
|
+
await handler.execute_write_query("INSERT INTO users (name) VALUES ('test')")
|
433
|
+
|
434
|
+
# Verify stats were updated
|
435
|
+
assert handler.stats.query_count == 1
|
436
|
+
assert handler.stats.error_count == 1
|
437
|
+
assert "Exception" in handler.stats.error_types
|
438
|
+
|
439
|
+
@pytest.mark.asyncio
|
440
|
+
async def test_execute_write_query_unsupported_operation(self, handler):
|
441
|
+
"""Test execute_write_query method with unsupported operation"""
|
442
|
+
with pytest.raises(ValueError, match=UNSUPPORTED_WRITE_OPERATION_ERROR.format(operation="SELECT")):
|
443
|
+
await handler.execute_write_query("SELECT * FROM users")
|
444
|
+
|
445
|
+
@pytest.mark.asyncio
|
446
|
+
async def test_execute_tool_query(self, handler):
|
447
|
+
"""Test execute_tool_query method"""
|
448
|
+
# Mock the tool methods
|
449
|
+
handler.get_table_description = AsyncMock(return_value="Table description")
|
450
|
+
handler.get_table_ddl = AsyncMock(return_value="Table DDL")
|
451
|
+
handler.get_table_indexes = AsyncMock(return_value="Table indexes")
|
452
|
+
handler.get_table_stats = AsyncMock(return_value="Table stats")
|
453
|
+
handler.get_table_constraints = AsyncMock(return_value="Table constraints")
|
454
|
+
handler.explain_query = AsyncMock(return_value="Query explanation")
|
455
|
+
|
456
|
+
# Test each tool
|
457
|
+
result1 = await handler.execute_tool_query("dbutils-describe-table", table_name="users")
|
458
|
+
assert result1 == "[mock]\nTable description"
|
459
|
+
handler.get_table_description.assert_called_once_with("users")
|
460
|
+
|
461
|
+
result2 = await handler.execute_tool_query("dbutils-get-ddl", table_name="users")
|
462
|
+
assert result2 == "[mock]\nTable DDL"
|
463
|
+
handler.get_table_ddl.assert_called_once_with("users")
|
464
|
+
|
465
|
+
result3 = await handler.execute_tool_query("dbutils-list-indexes", table_name="users")
|
466
|
+
assert result3 == "[mock]\nTable indexes"
|
467
|
+
handler.get_table_indexes.assert_called_once_with("users")
|
468
|
+
|
469
|
+
result4 = await handler.execute_tool_query("dbutils-get-stats", table_name="users")
|
470
|
+
assert result4 == "[mock]\nTable stats"
|
471
|
+
handler.get_table_stats.assert_called_once_with("users")
|
472
|
+
|
473
|
+
result5 = await handler.execute_tool_query("dbutils-list-constraints", table_name="users")
|
474
|
+
assert result5 == "[mock]\nTable constraints"
|
475
|
+
handler.get_table_constraints.assert_called_once_with("users")
|
476
|
+
|
477
|
+
result6 = await handler.execute_tool_query("dbutils-explain-query", sql="SELECT * FROM users")
|
478
|
+
assert result6 == "[mock]\nQuery explanation"
|
479
|
+
handler.explain_query.assert_called_once_with("SELECT * FROM users")
|
480
|
+
|
481
|
+
@pytest.mark.asyncio
|
482
|
+
async def test_execute_tool_query_unknown_tool(self, handler):
|
483
|
+
"""Test execute_tool_query method with unknown tool"""
|
484
|
+
with pytest.raises(ValueError, match="Unknown tool"):
|
485
|
+
await handler.execute_tool_query("unknown-tool")
|
486
|
+
|
487
|
+
@pytest.mark.asyncio
|
488
|
+
async def test_execute_tool_query_missing_sql(self, handler):
|
489
|
+
"""Test execute_tool_query method with missing SQL"""
|
490
|
+
with pytest.raises(ValueError):
|
491
|
+
await handler.execute_tool_query("dbutils-explain-query")
|
492
|
+
|
493
|
+
@pytest.mark.asyncio
|
494
|
+
async def test_execute_tool_query_error(self, handler):
|
495
|
+
"""Test execute_tool_query method with error"""
|
496
|
+
# Mock the tool method to raise error
|
497
|
+
handler.get_table_description = AsyncMock(side_effect=Exception("Tool error"))
|
498
|
+
|
499
|
+
with pytest.raises(Exception, match="Tool error"):
|
500
|
+
await handler.execute_tool_query("dbutils-describe-table", table_name="users")
|
501
|
+
|
502
|
+
# Verify stats were updated
|
503
|
+
assert handler.stats.error_count == 1
|
504
|
+
assert "Exception" in handler.stats.error_types
|