This is page 51 of 53. Use http://codebase.md/googleapis/genai-toolbox?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .ci
│ ├── continuous.release.cloudbuild.yaml
│ ├── generate_release_table.sh
│ ├── integration.cloudbuild.yaml
│ ├── quickstart_test
│ │ ├── go.integration.cloudbuild.yaml
│ │ ├── js.integration.cloudbuild.yaml
│ │ ├── py.integration.cloudbuild.yaml
│ │ ├── run_go_tests.sh
│ │ ├── run_js_tests.sh
│ │ ├── run_py_tests.sh
│ │ └── setup_hotels_sample.sql
│ ├── test_with_coverage.sh
│ └── versioned.release.cloudbuild.yaml
├── .github
│ ├── auto-label.yaml
│ ├── blunderbuss.yml
│ ├── CODEOWNERS
│ ├── header-checker-lint.yml
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ ├── feature_request.yml
│ │ └── question.yml
│ ├── label-sync.yml
│ ├── labels.yaml
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── release-please.yml
│ ├── renovate.json5
│ ├── sync-repo-settings.yaml
│ └── workflows
│ ├── cloud_build_failure_reporter.yml
│ ├── deploy_dev_docs.yaml
│ ├── deploy_previous_version_docs.yaml
│ ├── deploy_versioned_docs.yaml
│ ├── docs_deploy.yaml
│ ├── docs_preview_clean.yaml
│ ├── docs_preview_deploy.yaml
│ ├── lint.yaml
│ ├── schedule_reporter.yml
│ ├── sync-labels.yaml
│ └── tests.yaml
├── .gitignore
├── .gitmodules
├── .golangci.yaml
├── .hugo
│ ├── archetypes
│ │ └── default.md
│ ├── assets
│ │ ├── icons
│ │ │ └── logo.svg
│ │ └── scss
│ │ ├── _styles_project.scss
│ │ └── _variables_project.scss
│ ├── go.mod
│ ├── go.sum
│ ├── hugo.toml
│ ├── layouts
│ │ ├── _default
│ │ │ └── home.releases.releases
│ │ ├── index.llms-full.txt
│ │ ├── index.llms.txt
│ │ ├── partials
│ │ │ ├── hooks
│ │ │ │ └── head-end.html
│ │ │ ├── navbar-version-selector.html
│ │ │ ├── page-meta-links.html
│ │ │ └── td
│ │ │ └── render-heading.html
│ │ ├── robot.txt
│ │ └── shortcodes
│ │ ├── include.html
│ │ ├── ipynb.html
│ │ └── regionInclude.html
│ ├── package-lock.json
│ ├── package.json
│ └── static
│ ├── favicons
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── apple-touch-icon.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ └── favicon.ico
│ └── js
│ └── w3.js
├── CHANGELOG.md
├── cmd
│ ├── options_test.go
│ ├── options.go
│ ├── root_test.go
│ ├── root.go
│ └── version.txt
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── DEVELOPER.md
├── Dockerfile
├── docs
│ └── en
│ ├── _index.md
│ ├── about
│ │ ├── _index.md
│ │ └── faq.md
│ ├── concepts
│ │ ├── _index.md
│ │ └── telemetry
│ │ ├── index.md
│ │ ├── telemetry_flow.png
│ │ └── telemetry_traces.png
│ ├── getting-started
│ │ ├── _index.md
│ │ ├── colab_quickstart.ipynb
│ │ ├── configure.md
│ │ ├── introduction
│ │ │ ├── _index.md
│ │ │ └── architecture.png
│ │ ├── local_quickstart_go.md
│ │ ├── local_quickstart_js.md
│ │ ├── local_quickstart.md
│ │ ├── mcp_quickstart
│ │ │ ├── _index.md
│ │ │ ├── inspector_tools.png
│ │ │ └── inspector.png
│ │ └── quickstart
│ │ ├── go
│ │ │ ├── adkgo
│ │ │ │ ├── go.mod
│ │ │ │ ├── go.sum
│ │ │ │ └── quickstart.go
│ │ │ ├── genAI
│ │ │ │ ├── go.mod
│ │ │ │ ├── go.sum
│ │ │ │ └── quickstart.go
│ │ │ ├── genkit
│ │ │ │ ├── go.mod
│ │ │ │ ├── go.sum
│ │ │ │ └── quickstart.go
│ │ │ ├── langchain
│ │ │ │ ├── go.mod
│ │ │ │ ├── go.sum
│ │ │ │ └── quickstart.go
│ │ │ ├── openAI
│ │ │ │ ├── go.mod
│ │ │ │ ├── go.sum
│ │ │ │ └── quickstart.go
│ │ │ └── quickstart_test.go
│ │ ├── golden.txt
│ │ ├── js
│ │ │ ├── genAI
│ │ │ │ ├── package-lock.json
│ │ │ │ ├── package.json
│ │ │ │ └── quickstart.js
│ │ │ ├── genkit
│ │ │ │ ├── package-lock.json
│ │ │ │ ├── package.json
│ │ │ │ └── quickstart.js
│ │ │ ├── langchain
│ │ │ │ ├── package-lock.json
│ │ │ │ ├── package.json
│ │ │ │ └── quickstart.js
│ │ │ ├── llamaindex
│ │ │ │ ├── package-lock.json
│ │ │ │ ├── package.json
│ │ │ │ └── quickstart.js
│ │ │ └── quickstart.test.js
│ │ ├── python
│ │ │ ├── __init__.py
│ │ │ ├── adk
│ │ │ │ ├── quickstart.py
│ │ │ │ └── requirements.txt
│ │ │ ├── core
│ │ │ │ ├── quickstart.py
│ │ │ │ └── requirements.txt
│ │ │ ├── langchain
│ │ │ │ ├── quickstart.py
│ │ │ │ └── requirements.txt
│ │ │ ├── llamaindex
│ │ │ │ ├── quickstart.py
│ │ │ │ └── requirements.txt
│ │ │ └── quickstart_test.py
│ │ └── shared
│ │ ├── cloud_setup.md
│ │ ├── configure_toolbox.md
│ │ └── database_setup.md
│ ├── how-to
│ │ ├── _index.md
│ │ ├── connect_via_geminicli.md
│ │ ├── connect_via_mcp.md
│ │ ├── connect-ide
│ │ │ ├── _index.md
│ │ │ ├── alloydb_pg_admin_mcp.md
│ │ │ ├── alloydb_pg_mcp.md
│ │ │ ├── bigquery_mcp.md
│ │ │ ├── cloud_sql_mssql_admin_mcp.md
│ │ │ ├── cloud_sql_mssql_mcp.md
│ │ │ ├── cloud_sql_mysql_admin_mcp.md
│ │ │ ├── cloud_sql_mysql_mcp.md
│ │ │ ├── cloud_sql_pg_admin_mcp.md
│ │ │ ├── cloud_sql_pg_mcp.md
│ │ │ ├── firestore_mcp.md
│ │ │ ├── looker_mcp.md
│ │ │ ├── mssql_mcp.md
│ │ │ ├── mysql_mcp.md
│ │ │ ├── neo4j_mcp.md
│ │ │ ├── postgres_mcp.md
│ │ │ ├── spanner_mcp.md
│ │ │ └── sqlite_mcp.md
│ │ ├── deploy_docker.md
│ │ ├── deploy_gke.md
│ │ ├── deploy_toolbox.md
│ │ ├── export_telemetry.md
│ │ └── toolbox-ui
│ │ ├── edit-headers.gif
│ │ ├── edit-headers.png
│ │ ├── index.md
│ │ ├── optional-param-checked.png
│ │ ├── optional-param-unchecked.png
│ │ ├── run-tool.gif
│ │ ├── tools.png
│ │ └── toolsets.png
│ ├── reference
│ │ ├── _index.md
│ │ ├── cli.md
│ │ └── prebuilt-tools.md
│ ├── resources
│ │ ├── _index.md
│ │ ├── authServices
│ │ │ ├── _index.md
│ │ │ └── google.md
│ │ ├── sources
│ │ │ ├── _index.md
│ │ │ ├── alloydb-admin.md
│ │ │ ├── alloydb-pg.md
│ │ │ ├── bigquery.md
│ │ │ ├── bigtable.md
│ │ │ ├── cassandra.md
│ │ │ ├── clickhouse.md
│ │ │ ├── cloud-monitoring.md
│ │ │ ├── cloud-sql-admin.md
│ │ │ ├── cloud-sql-mssql.md
│ │ │ ├── cloud-sql-mysql.md
│ │ │ ├── cloud-sql-pg.md
│ │ │ ├── couchbase.md
│ │ │ ├── dataplex.md
│ │ │ ├── dgraph.md
│ │ │ ├── elasticsearch.md
│ │ │ ├── firebird.md
│ │ │ ├── firestore.md
│ │ │ ├── http.md
│ │ │ ├── looker.md
│ │ │ ├── mindsdb.md
│ │ │ ├── mongodb.md
│ │ │ ├── mssql.md
│ │ │ ├── mysql.md
│ │ │ ├── neo4j.md
│ │ │ ├── oceanbase.md
│ │ │ ├── oracle.md
│ │ │ ├── postgres.md
│ │ │ ├── redis.md
│ │ │ ├── serverless-spark.md
│ │ │ ├── spanner.md
│ │ │ ├── sqlite.md
│ │ │ ├── tidb.md
│ │ │ ├── trino.md
│ │ │ ├── valkey.md
│ │ │ └── yugabytedb.md
│ │ └── tools
│ │ ├── _index.md
│ │ ├── alloydb
│ │ │ ├── _index.md
│ │ │ ├── alloydb-create-cluster.md
│ │ │ ├── alloydb-create-instance.md
│ │ │ ├── alloydb-create-user.md
│ │ │ ├── alloydb-get-cluster.md
│ │ │ ├── alloydb-get-instance.md
│ │ │ ├── alloydb-get-user.md
│ │ │ ├── alloydb-list-clusters.md
│ │ │ ├── alloydb-list-instances.md
│ │ │ ├── alloydb-list-users.md
│ │ │ └── alloydb-wait-for-operation.md
│ │ ├── alloydbainl
│ │ │ ├── _index.md
│ │ │ └── alloydb-ai-nl.md
│ │ ├── bigquery
│ │ │ ├── _index.md
│ │ │ ├── bigquery-analyze-contribution.md
│ │ │ ├── bigquery-conversational-analytics.md
│ │ │ ├── bigquery-execute-sql.md
│ │ │ ├── bigquery-forecast.md
│ │ │ ├── bigquery-get-dataset-info.md
│ │ │ ├── bigquery-get-table-info.md
│ │ │ ├── bigquery-list-dataset-ids.md
│ │ │ ├── bigquery-list-table-ids.md
│ │ │ ├── bigquery-search-catalog.md
│ │ │ └── bigquery-sql.md
│ │ ├── bigtable
│ │ │ ├── _index.md
│ │ │ └── bigtable-sql.md
│ │ ├── cassandra
│ │ │ ├── _index.md
│ │ │ └── cassandra-cql.md
│ │ ├── clickhouse
│ │ │ ├── _index.md
│ │ │ ├── clickhouse-execute-sql.md
│ │ │ ├── clickhouse-list-databases.md
│ │ │ ├── clickhouse-list-tables.md
│ │ │ └── clickhouse-sql.md
│ │ ├── cloudmonitoring
│ │ │ ├── _index.md
│ │ │ └── cloud-monitoring-query-prometheus.md
│ │ ├── cloudsql
│ │ │ ├── _index.md
│ │ │ ├── cloudsqlcreatedatabase.md
│ │ │ ├── cloudsqlcreateusers.md
│ │ │ ├── cloudsqlgetinstances.md
│ │ │ ├── cloudsqllistdatabases.md
│ │ │ ├── cloudsqllistinstances.md
│ │ │ ├── cloudsqlmssqlcreateinstance.md
│ │ │ ├── cloudsqlmysqlcreateinstance.md
│ │ │ ├── cloudsqlpgcreateinstances.md
│ │ │ └── cloudsqlwaitforoperation.md
│ │ ├── couchbase
│ │ │ ├── _index.md
│ │ │ └── couchbase-sql.md
│ │ ├── dataform
│ │ │ ├── _index.md
│ │ │ └── dataform-compile-local.md
│ │ ├── dataplex
│ │ │ ├── _index.md
│ │ │ ├── dataplex-lookup-entry.md
│ │ │ ├── dataplex-search-aspect-types.md
│ │ │ └── dataplex-search-entries.md
│ │ ├── dgraph
│ │ │ ├── _index.md
│ │ │ └── dgraph-dql.md
│ │ ├── elasticsearch
│ │ │ ├── _index.md
│ │ │ └── elasticsearch-esql.md
│ │ ├── firebird
│ │ │ ├── _index.md
│ │ │ ├── firebird-execute-sql.md
│ │ │ └── firebird-sql.md
│ │ ├── firestore
│ │ │ ├── _index.md
│ │ │ ├── firestore-add-documents.md
│ │ │ ├── firestore-delete-documents.md
│ │ │ ├── firestore-get-documents.md
│ │ │ ├── firestore-get-rules.md
│ │ │ ├── firestore-list-collections.md
│ │ │ ├── firestore-query-collection.md
│ │ │ ├── firestore-query.md
│ │ │ ├── firestore-update-document.md
│ │ │ └── firestore-validate-rules.md
│ │ ├── http
│ │ │ ├── _index.md
│ │ │ └── http.md
│ │ ├── looker
│ │ │ ├── _index.md
│ │ │ ├── looker-add-dashboard-element.md
│ │ │ ├── looker-conversational-analytics.md
│ │ │ ├── looker-create-project-file.md
│ │ │ ├── looker-delete-project-file.md
│ │ │ ├── looker-dev-mode.md
│ │ │ ├── looker-get-connection-databases.md
│ │ │ ├── looker-get-connection-schemas.md
│ │ │ ├── looker-get-connection-table-columns.md
│ │ │ ├── looker-get-connection-tables.md
│ │ │ ├── looker-get-connections.md
│ │ │ ├── looker-get-dashboards.md
│ │ │ ├── looker-get-dimensions.md
│ │ │ ├── looker-get-explores.md
│ │ │ ├── looker-get-filters.md
│ │ │ ├── looker-get-looks.md
│ │ │ ├── looker-get-measures.md
│ │ │ ├── looker-get-models.md
│ │ │ ├── looker-get-parameters.md
│ │ │ ├── looker-get-project-file.md
│ │ │ ├── looker-get-project-files.md
│ │ │ ├── looker-get-projects.md
│ │ │ ├── looker-health-analyze.md
│ │ │ ├── looker-health-pulse.md
│ │ │ ├── looker-health-vacuum.md
│ │ │ ├── looker-make-dashboard.md
│ │ │ ├── looker-make-look.md
│ │ │ ├── looker-query-sql.md
│ │ │ ├── looker-query-url.md
│ │ │ ├── looker-query.md
│ │ │ ├── looker-run-look.md
│ │ │ └── looker-update-project-file.md
│ │ ├── mindsdb
│ │ │ ├── _index.md
│ │ │ ├── mindsdb-execute-sql.md
│ │ │ └── mindsdb-sql.md
│ │ ├── mongodb
│ │ │ ├── _index.md
│ │ │ ├── mongodb-aggregate.md
│ │ │ ├── mongodb-delete-many.md
│ │ │ ├── mongodb-delete-one.md
│ │ │ ├── mongodb-find-one.md
│ │ │ ├── mongodb-find.md
│ │ │ ├── mongodb-insert-many.md
│ │ │ ├── mongodb-insert-one.md
│ │ │ ├── mongodb-update-many.md
│ │ │ └── mongodb-update-one.md
│ │ ├── mssql
│ │ │ ├── _index.md
│ │ │ ├── mssql-execute-sql.md
│ │ │ ├── mssql-list-tables.md
│ │ │ └── mssql-sql.md
│ │ ├── mysql
│ │ │ ├── _index.md
│ │ │ ├── mysql-execute-sql.md
│ │ │ ├── mysql-list-active-queries.md
│ │ │ ├── mysql-list-table-fragmentation.md
│ │ │ ├── mysql-list-tables-missing-unique-indexes.md
│ │ │ ├── mysql-list-tables.md
│ │ │ └── mysql-sql.md
│ │ ├── neo4j
│ │ │ ├── _index.md
│ │ │ ├── neo4j-cypher.md
│ │ │ ├── neo4j-execute-cypher.md
│ │ │ └── neo4j-schema.md
│ │ ├── oceanbase
│ │ │ ├── _index.md
│ │ │ ├── oceanbase-execute-sql.md
│ │ │ └── oceanbase-sql.md
│ │ ├── oracle
│ │ │ ├── _index.md
│ │ │ ├── oracle-execute-sql.md
│ │ │ └── oracle-sql.md
│ │ ├── postgres
│ │ │ ├── _index.md
│ │ │ ├── postgres-execute-sql.md
│ │ │ ├── postgres-list-active-queries.md
│ │ │ ├── postgres-list-available-extensions.md
│ │ │ ├── postgres-list-installed-extensions.md
│ │ │ ├── postgres-list-schemas.md
│ │ │ ├── postgres-list-tables.md
│ │ │ ├── postgres-list-views.md
│ │ │ └── postgres-sql.md
│ │ ├── redis
│ │ │ ├── _index.md
│ │ │ └── redis.md
│ │ ├── serverless-spark
│ │ │ ├── _index.md
│ │ │ ├── serverless-spark-cancel-batch.md
│ │ │ ├── serverless-spark-get-batch.md
│ │ │ └── serverless-spark-list-batches.md
│ │ ├── spanner
│ │ │ ├── _index.md
│ │ │ ├── spanner-execute-sql.md
│ │ │ ├── spanner-list-tables.md
│ │ │ └── spanner-sql.md
│ │ ├── sqlite
│ │ │ ├── _index.md
│ │ │ ├── sqlite-execute-sql.md
│ │ │ └── sqlite-sql.md
│ │ ├── tidb
│ │ │ ├── _index.md
│ │ │ ├── tidb-execute-sql.md
│ │ │ └── tidb-sql.md
│ │ ├── trino
│ │ │ ├── _index.md
│ │ │ ├── trino-execute-sql.md
│ │ │ └── trino-sql.md
│ │ ├── utility
│ │ │ ├── _index.md
│ │ │ └── wait.md
│ │ ├── valkey
│ │ │ ├── _index.md
│ │ │ └── valkey.md
│ │ └── yuagbytedb
│ │ ├── _index.md
│ │ └── yugabytedb-sql.md
│ ├── samples
│ │ ├── _index.md
│ │ ├── alloydb
│ │ │ ├── _index.md
│ │ │ ├── ai-nl
│ │ │ │ ├── alloydb_ai_nl.ipynb
│ │ │ │ └── index.md
│ │ │ └── mcp_quickstart.md
│ │ ├── bigquery
│ │ │ ├── _index.md
│ │ │ ├── colab_quickstart_bigquery.ipynb
│ │ │ ├── local_quickstart.md
│ │ │ └── mcp_quickstart
│ │ │ ├── _index.md
│ │ │ ├── inspector_tools.png
│ │ │ └── inspector.png
│ │ └── looker
│ │ ├── _index.md
│ │ ├── looker_gemini_oauth
│ │ │ ├── _index.md
│ │ │ ├── authenticated.png
│ │ │ ├── authorize.png
│ │ │ └── registration.png
│ │ ├── looker_gemini.md
│ │ └── looker_mcp_inspector
│ │ ├── _index.md
│ │ ├── inspector_tools.png
│ │ └── inspector.png
│ └── sdks
│ ├── _index.md
│ ├── go-sdk.md
│ ├── js-sdk.md
│ └── python-sdk.md
├── gemini-extension.json
├── go.mod
├── go.sum
├── internal
│ ├── auth
│ │ ├── auth.go
│ │ └── google
│ │ └── google.go
│ ├── log
│ │ ├── handler.go
│ │ ├── log_test.go
│ │ ├── log.go
│ │ └── logger.go
│ ├── prebuiltconfigs
│ │ ├── prebuiltconfigs_test.go
│ │ ├── prebuiltconfigs.go
│ │ └── tools
│ │ ├── alloydb-postgres-admin.yaml
│ │ ├── alloydb-postgres-observability.yaml
│ │ ├── alloydb-postgres.yaml
│ │ ├── bigquery.yaml
│ │ ├── clickhouse.yaml
│ │ ├── cloud-sql-mssql-admin.yaml
│ │ ├── cloud-sql-mssql-observability.yaml
│ │ ├── cloud-sql-mssql.yaml
│ │ ├── cloud-sql-mysql-admin.yaml
│ │ ├── cloud-sql-mysql-observability.yaml
│ │ ├── cloud-sql-mysql.yaml
│ │ ├── cloud-sql-postgres-admin.yaml
│ │ ├── cloud-sql-postgres-observability.yaml
│ │ ├── cloud-sql-postgres.yaml
│ │ ├── dataplex.yaml
│ │ ├── elasticsearch.yaml
│ │ ├── firestore.yaml
│ │ ├── looker-conversational-analytics.yaml
│ │ ├── looker.yaml
│ │ ├── mindsdb.yaml
│ │ ├── mssql.yaml
│ │ ├── mysql.yaml
│ │ ├── neo4j.yaml
│ │ ├── oceanbase.yaml
│ │ ├── postgres.yaml
│ │ ├── serverless-spark.yaml
│ │ ├── spanner-postgres.yaml
│ │ ├── spanner.yaml
│ │ └── sqlite.yaml
│ ├── server
│ │ ├── api_test.go
│ │ ├── api.go
│ │ ├── common_test.go
│ │ ├── config.go
│ │ ├── mcp
│ │ │ ├── jsonrpc
│ │ │ │ ├── jsonrpc_test.go
│ │ │ │ └── jsonrpc.go
│ │ │ ├── mcp.go
│ │ │ ├── util
│ │ │ │ └── lifecycle.go
│ │ │ ├── v20241105
│ │ │ │ ├── method.go
│ │ │ │ └── types.go
│ │ │ ├── v20250326
│ │ │ │ ├── method.go
│ │ │ │ └── types.go
│ │ │ └── v20250618
│ │ │ ├── method.go
│ │ │ └── types.go
│ │ ├── mcp_test.go
│ │ ├── mcp.go
│ │ ├── server_test.go
│ │ ├── server.go
│ │ ├── static
│ │ │ ├── assets
│ │ │ │ └── mcptoolboxlogo.png
│ │ │ ├── css
│ │ │ │ └── style.css
│ │ │ ├── index.html
│ │ │ ├── js
│ │ │ │ ├── auth.js
│ │ │ │ ├── loadTools.js
│ │ │ │ ├── mainContent.js
│ │ │ │ ├── navbar.js
│ │ │ │ ├── runTool.js
│ │ │ │ ├── toolDisplay.js
│ │ │ │ ├── tools.js
│ │ │ │ └── toolsets.js
│ │ │ ├── tools.html
│ │ │ └── toolsets.html
│ │ ├── web_test.go
│ │ └── web.go
│ ├── sources
│ │ ├── alloydbadmin
│ │ │ ├── alloydbadmin_test.go
│ │ │ └── alloydbadmin.go
│ │ ├── alloydbpg
│ │ │ ├── alloydb_pg_test.go
│ │ │ └── alloydb_pg.go
│ │ ├── bigquery
│ │ │ ├── bigquery_test.go
│ │ │ ├── bigquery.go
│ │ │ └── cache.go
│ │ ├── bigtable
│ │ │ ├── bigtable_test.go
│ │ │ └── bigtable.go
│ │ ├── cassandra
│ │ │ ├── cassandra_test.go
│ │ │ └── cassandra.go
│ │ ├── clickhouse
│ │ │ ├── clickhouse_test.go
│ │ │ └── clickhouse.go
│ │ ├── cloudmonitoring
│ │ │ ├── cloud_monitoring_test.go
│ │ │ └── cloud_monitoring.go
│ │ ├── cloudsqladmin
│ │ │ ├── cloud_sql_admin_test.go
│ │ │ └── cloud_sql_admin.go
│ │ ├── cloudsqlmssql
│ │ │ ├── cloud_sql_mssql_test.go
│ │ │ └── cloud_sql_mssql.go
│ │ ├── cloudsqlmysql
│ │ │ ├── cloud_sql_mysql_test.go
│ │ │ └── cloud_sql_mysql.go
│ │ ├── cloudsqlpg
│ │ │ ├── cloud_sql_pg_test.go
│ │ │ └── cloud_sql_pg.go
│ │ ├── couchbase
│ │ │ ├── couchbase_test.go
│ │ │ └── couchbase.go
│ │ ├── dataplex
│ │ │ ├── dataplex_test.go
│ │ │ └── dataplex.go
│ │ ├── dgraph
│ │ │ ├── dgraph_test.go
│ │ │ └── dgraph.go
│ │ ├── dialect.go
│ │ ├── elasticsearch
│ │ │ ├── elasticsearch_test.go
│ │ │ └── elasticsearch.go
│ │ ├── firebird
│ │ │ ├── firebird_test.go
│ │ │ └── firebird.go
│ │ ├── firestore
│ │ │ ├── firestore_test.go
│ │ │ └── firestore.go
│ │ ├── http
│ │ │ ├── http_test.go
│ │ │ └── http.go
│ │ ├── ip_type.go
│ │ ├── looker
│ │ │ ├── looker_test.go
│ │ │ └── looker.go
│ │ ├── mindsdb
│ │ │ ├── mindsdb_test.go
│ │ │ └── mindsdb.go
│ │ ├── mongodb
│ │ │ ├── mongodb_test.go
│ │ │ └── mongodb.go
│ │ ├── mssql
│ │ │ ├── mssql_test.go
│ │ │ └── mssql.go
│ │ ├── mysql
│ │ │ ├── mysql_test.go
│ │ │ └── mysql.go
│ │ ├── neo4j
│ │ │ ├── neo4j_test.go
│ │ │ └── neo4j.go
│ │ ├── oceanbase
│ │ │ ├── oceanbase_test.go
│ │ │ └── oceanbase.go
│ │ ├── oracle
│ │ │ └── oracle.go
│ │ ├── postgres
│ │ │ ├── postgres_test.go
│ │ │ └── postgres.go
│ │ ├── redis
│ │ │ ├── redis_test.go
│ │ │ └── redis.go
│ │ ├── serverlessspark
│ │ │ ├── serverlessspark_test.go
│ │ │ └── serverlessspark.go
│ │ ├── sources.go
│ │ ├── spanner
│ │ │ ├── spanner_test.go
│ │ │ └── spanner.go
│ │ ├── sqlite
│ │ │ ├── sqlite_test.go
│ │ │ └── sqlite.go
│ │ ├── tidb
│ │ │ ├── tidb_test.go
│ │ │ └── tidb.go
│ │ ├── trino
│ │ │ ├── trino_test.go
│ │ │ └── trino.go
│ │ ├── util.go
│ │ ├── valkey
│ │ │ ├── valkey_test.go
│ │ │ └── valkey.go
│ │ └── yugabytedb
│ │ ├── yugabytedb_test.go
│ │ └── yugabytedb.go
│ ├── telemetry
│ │ ├── instrumentation.go
│ │ └── telemetry.go
│ ├── testutils
│ │ └── testutils.go
│ ├── tools
│ │ ├── alloydb
│ │ │ ├── alloydbcreatecluster
│ │ │ │ ├── alloydbcreatecluster_test.go
│ │ │ │ └── alloydbcreatecluster.go
│ │ │ ├── alloydbcreateinstance
│ │ │ │ ├── alloydbcreateinstance_test.go
│ │ │ │ └── alloydbcreateinstance.go
│ │ │ ├── alloydbcreateuser
│ │ │ │ ├── alloydbcreateuser_test.go
│ │ │ │ └── alloydbcreateuser.go
│ │ │ ├── alloydbgetcluster
│ │ │ │ ├── alloydbgetcluster_test.go
│ │ │ │ └── alloydbgetcluster.go
│ │ │ ├── alloydbgetinstance
│ │ │ │ ├── alloydbgetinstance_test.go
│ │ │ │ └── alloydbgetinstance.go
│ │ │ ├── alloydbgetuser
│ │ │ │ ├── alloydbgetuser_test.go
│ │ │ │ └── alloydbgetuser.go
│ │ │ ├── alloydblistclusters
│ │ │ │ ├── alloydblistclusters_test.go
│ │ │ │ └── alloydblistclusters.go
│ │ │ ├── alloydblistinstances
│ │ │ │ ├── alloydblistinstances_test.go
│ │ │ │ └── alloydblistinstances.go
│ │ │ ├── alloydblistusers
│ │ │ │ ├── alloydblistusers_test.go
│ │ │ │ └── alloydblistusers.go
│ │ │ └── alloydbwaitforoperation
│ │ │ ├── alloydbwaitforoperation_test.go
│ │ │ └── alloydbwaitforoperation.go
│ │ ├── alloydbainl
│ │ │ ├── alloydbainl_test.go
│ │ │ └── alloydbainl.go
│ │ ├── bigquery
│ │ │ ├── bigqueryanalyzecontribution
│ │ │ │ ├── bigqueryanalyzecontribution_test.go
│ │ │ │ └── bigqueryanalyzecontribution.go
│ │ │ ├── bigquerycommon
│ │ │ │ ├── table_name_parser_test.go
│ │ │ │ ├── table_name_parser.go
│ │ │ │ └── util.go
│ │ │ ├── bigqueryconversationalanalytics
│ │ │ │ ├── bigqueryconversationalanalytics_test.go
│ │ │ │ └── bigqueryconversationalanalytics.go
│ │ │ ├── bigqueryexecutesql
│ │ │ │ ├── bigqueryexecutesql_test.go
│ │ │ │ └── bigqueryexecutesql.go
│ │ │ ├── bigqueryforecast
│ │ │ │ ├── bigqueryforecast_test.go
│ │ │ │ └── bigqueryforecast.go
│ │ │ ├── bigquerygetdatasetinfo
│ │ │ │ ├── bigquerygetdatasetinfo_test.go
│ │ │ │ └── bigquerygetdatasetinfo.go
│ │ │ ├── bigquerygettableinfo
│ │ │ │ ├── bigquerygettableinfo_test.go
│ │ │ │ └── bigquerygettableinfo.go
│ │ │ ├── bigquerylistdatasetids
│ │ │ │ ├── bigquerylistdatasetids_test.go
│ │ │ │ └── bigquerylistdatasetids.go
│ │ │ ├── bigquerylisttableids
│ │ │ │ ├── bigquerylisttableids_test.go
│ │ │ │ └── bigquerylisttableids.go
│ │ │ ├── bigquerysearchcatalog
│ │ │ │ ├── bigquerysearchcatalog_test.go
│ │ │ │ └── bigquerysearchcatalog.go
│ │ │ └── bigquerysql
│ │ │ ├── bigquerysql_test.go
│ │ │ └── bigquerysql.go
│ │ ├── bigtable
│ │ │ ├── bigtable_test.go
│ │ │ └── bigtable.go
│ │ ├── cassandra
│ │ │ └── cassandracql
│ │ │ ├── cassandracql_test.go
│ │ │ └── cassandracql.go
│ │ ├── clickhouse
│ │ │ ├── clickhouseexecutesql
│ │ │ │ ├── clickhouseexecutesql_test.go
│ │ │ │ └── clickhouseexecutesql.go
│ │ │ ├── clickhouselistdatabases
│ │ │ │ ├── clickhouselistdatabases_test.go
│ │ │ │ └── clickhouselistdatabases.go
│ │ │ ├── clickhouselisttables
│ │ │ │ ├── clickhouselisttables_test.go
│ │ │ │ └── clickhouselisttables.go
│ │ │ └── clickhousesql
│ │ │ ├── clickhousesql_test.go
│ │ │ └── clickhousesql.go
│ │ ├── cloudmonitoring
│ │ │ ├── cloudmonitoring_test.go
│ │ │ └── cloudmonitoring.go
│ │ ├── cloudsql
│ │ │ ├── cloudsqlcreatedatabase
│ │ │ │ ├── cloudsqlcreatedatabase_test.go
│ │ │ │ └── cloudsqlcreatedatabase.go
│ │ │ ├── cloudsqlcreateusers
│ │ │ │ ├── cloudsqlcreateusers_test.go
│ │ │ │ └── cloudsqlcreateusers.go
│ │ │ ├── cloudsqlgetinstances
│ │ │ │ ├── cloudsqlgetinstances_test.go
│ │ │ │ └── cloudsqlgetinstances.go
│ │ │ ├── cloudsqllistdatabases
│ │ │ │ ├── cloudsqllistdatabases_test.go
│ │ │ │ └── cloudsqllistdatabases.go
│ │ │ ├── cloudsqllistinstances
│ │ │ │ ├── cloudsqllistinstances_test.go
│ │ │ │ └── cloudsqllistinstances.go
│ │ │ └── cloudsqlwaitforoperation
│ │ │ ├── cloudsqlwaitforoperation_test.go
│ │ │ └── cloudsqlwaitforoperation.go
│ │ ├── cloudsqlmssql
│ │ │ └── cloudsqlmssqlcreateinstance
│ │ │ ├── cloudsqlmssqlcreateinstance_test.go
│ │ │ └── cloudsqlmssqlcreateinstance.go
│ │ ├── cloudsqlmysql
│ │ │ └── cloudsqlmysqlcreateinstance
│ │ │ ├── cloudsqlmysqlcreateinstance_test.go
│ │ │ └── cloudsqlmysqlcreateinstance.go
│ │ ├── cloudsqlpg
│ │ │ └── cloudsqlpgcreateinstances
│ │ │ ├── cloudsqlpgcreateinstances_test.go
│ │ │ └── cloudsqlpgcreateinstances.go
│ │ ├── common_test.go
│ │ ├── common.go
│ │ ├── couchbase
│ │ │ ├── couchbase_test.go
│ │ │ └── couchbase.go
│ │ ├── dataform
│ │ │ └── dataformcompilelocal
│ │ │ ├── dataformcompilelocal_test.go
│ │ │ └── dataformcompilelocal.go
│ │ ├── dataplex
│ │ │ ├── dataplexlookupentry
│ │ │ │ ├── dataplexlookupentry_test.go
│ │ │ │ └── dataplexlookupentry.go
│ │ │ ├── dataplexsearchaspecttypes
│ │ │ │ ├── dataplexsearchaspecttypes_test.go
│ │ │ │ └── dataplexsearchaspecttypes.go
│ │ │ └── dataplexsearchentries
│ │ │ ├── dataplexsearchentries_test.go
│ │ │ └── dataplexsearchentries.go
│ │ ├── dgraph
│ │ │ ├── dgraph_test.go
│ │ │ └── dgraph.go
│ │ ├── elasticsearch
│ │ │ └── elasticsearchesql
│ │ │ ├── elasticsearchesql_test.go
│ │ │ └── elasticsearchesql.go
│ │ ├── firebird
│ │ │ ├── firebirdexecutesql
│ │ │ │ ├── firebirdexecutesql_test.go
│ │ │ │ └── firebirdexecutesql.go
│ │ │ └── firebirdsql
│ │ │ ├── firebirdsql_test.go
│ │ │ └── firebirdsql.go
│ │ ├── firestore
│ │ │ ├── firestoreadddocuments
│ │ │ │ ├── firestoreadddocuments_test.go
│ │ │ │ └── firestoreadddocuments.go
│ │ │ ├── firestoredeletedocuments
│ │ │ │ ├── firestoredeletedocuments_test.go
│ │ │ │ └── firestoredeletedocuments.go
│ │ │ ├── firestoregetdocuments
│ │ │ │ ├── firestoregetdocuments_test.go
│ │ │ │ └── firestoregetdocuments.go
│ │ │ ├── firestoregetrules
│ │ │ │ ├── firestoregetrules_test.go
│ │ │ │ └── firestoregetrules.go
│ │ │ ├── firestorelistcollections
│ │ │ │ ├── firestorelistcollections_test.go
│ │ │ │ └── firestorelistcollections.go
│ │ │ ├── firestorequery
│ │ │ │ ├── firestorequery_test.go
│ │ │ │ └── firestorequery.go
│ │ │ ├── firestorequerycollection
│ │ │ │ ├── firestorequerycollection_test.go
│ │ │ │ └── firestorequerycollection.go
│ │ │ ├── firestoreupdatedocument
│ │ │ │ ├── firestoreupdatedocument_test.go
│ │ │ │ └── firestoreupdatedocument.go
│ │ │ ├── firestorevalidaterules
│ │ │ │ ├── firestorevalidaterules_test.go
│ │ │ │ └── firestorevalidaterules.go
│ │ │ └── util
│ │ │ ├── converter_test.go
│ │ │ ├── converter.go
│ │ │ ├── validator_test.go
│ │ │ └── validator.go
│ │ ├── http
│ │ │ ├── http_test.go
│ │ │ └── http.go
│ │ ├── http_method.go
│ │ ├── looker
│ │ │ ├── lookeradddashboardelement
│ │ │ │ ├── lookeradddashboardelement_test.go
│ │ │ │ └── lookeradddashboardelement.go
│ │ │ ├── lookercommon
│ │ │ │ ├── lookercommon_test.go
│ │ │ │ └── lookercommon.go
│ │ │ ├── lookerconversationalanalytics
│ │ │ │ ├── lookerconversationalanalytics_test.go
│ │ │ │ └── lookerconversationalanalytics.go
│ │ │ ├── lookercreateprojectfile
│ │ │ │ ├── lookercreateprojectfile_test.go
│ │ │ │ └── lookercreateprojectfile.go
│ │ │ ├── lookerdeleteprojectfile
│ │ │ │ ├── lookerdeleteprojectfile_test.go
│ │ │ │ └── lookerdeleteprojectfile.go
│ │ │ ├── lookerdevmode
│ │ │ │ ├── lookerdevmode_test.go
│ │ │ │ └── lookerdevmode.go
│ │ │ ├── lookergetconnectiondatabases
│ │ │ │ ├── lookergetconnectiondatabases_test.go
│ │ │ │ └── lookergetconnectiondatabases.go
│ │ │ ├── lookergetconnections
│ │ │ │ ├── lookergetconnections_test.go
│ │ │ │ └── lookergetconnections.go
│ │ │ ├── lookergetconnectionschemas
│ │ │ │ ├── lookergetconnectionschemas_test.go
│ │ │ │ └── lookergetconnectionschemas.go
│ │ │ ├── lookergetconnectiontablecolumns
│ │ │ │ ├── lookergetconnectiontablecolumns_test.go
│ │ │ │ └── lookergetconnectiontablecolumns.go
│ │ │ ├── lookergetconnectiontables
│ │ │ │ ├── lookergetconnectiontables_test.go
│ │ │ │ └── lookergetconnectiontables.go
│ │ │ ├── lookergetdashboards
│ │ │ │ ├── lookergetdashboards_test.go
│ │ │ │ └── lookergetdashboards.go
│ │ │ ├── lookergetdimensions
│ │ │ │ ├── lookergetdimensions_test.go
│ │ │ │ └── lookergetdimensions.go
│ │ │ ├── lookergetexplores
│ │ │ │ ├── lookergetexplores_test.go
│ │ │ │ └── lookergetexplores.go
│ │ │ ├── lookergetfilters
│ │ │ │ ├── lookergetfilters_test.go
│ │ │ │ └── lookergetfilters.go
│ │ │ ├── lookergetlooks
│ │ │ │ ├── lookergetlooks_test.go
│ │ │ │ └── lookergetlooks.go
│ │ │ ├── lookergetmeasures
│ │ │ │ ├── lookergetmeasures_test.go
│ │ │ │ └── lookergetmeasures.go
│ │ │ ├── lookergetmodels
│ │ │ │ ├── lookergetmodels_test.go
│ │ │ │ └── lookergetmodels.go
│ │ │ ├── lookergetparameters
│ │ │ │ ├── lookergetparameters_test.go
│ │ │ │ └── lookergetparameters.go
│ │ │ ├── lookergetprojectfile
│ │ │ │ ├── lookergetprojectfile_test.go
│ │ │ │ └── lookergetprojectfile.go
│ │ │ ├── lookergetprojectfiles
│ │ │ │ ├── lookergetprojectfiles_test.go
│ │ │ │ └── lookergetprojectfiles.go
│ │ │ ├── lookergetprojects
│ │ │ │ ├── lookergetprojects_test.go
│ │ │ │ └── lookergetprojects.go
│ │ │ ├── lookerhealthanalyze
│ │ │ │ ├── lookerhealthanalyze_test.go
│ │ │ │ └── lookerhealthanalyze.go
│ │ │ ├── lookerhealthpulse
│ │ │ │ ├── lookerhealthpulse_test.go
│ │ │ │ └── lookerhealthpulse.go
│ │ │ ├── lookerhealthvacuum
│ │ │ │ ├── lookerhealthvacuum_test.go
│ │ │ │ └── lookerhealthvacuum.go
│ │ │ ├── lookermakedashboard
│ │ │ │ ├── lookermakedashboard_test.go
│ │ │ │ └── lookermakedashboard.go
│ │ │ ├── lookermakelook
│ │ │ │ ├── lookermakelook_test.go
│ │ │ │ └── lookermakelook.go
│ │ │ ├── lookerquery
│ │ │ │ ├── lookerquery_test.go
│ │ │ │ └── lookerquery.go
│ │ │ ├── lookerquerysql
│ │ │ │ ├── lookerquerysql_test.go
│ │ │ │ └── lookerquerysql.go
│ │ │ ├── lookerqueryurl
│ │ │ │ ├── lookerqueryurl_test.go
│ │ │ │ └── lookerqueryurl.go
│ │ │ ├── lookerrunlook
│ │ │ │ ├── lookerrunlook_test.go
│ │ │ │ └── lookerrunlook.go
│ │ │ └── lookerupdateprojectfile
│ │ │ ├── lookerupdateprojectfile_test.go
│ │ │ └── lookerupdateprojectfile.go
│ │ ├── mindsdb
│ │ │ ├── mindsdbexecutesql
│ │ │ │ ├── mindsdbexecutesql_test.go
│ │ │ │ └── mindsdbexecutesql.go
│ │ │ └── mindsdbsql
│ │ │ ├── mindsdbsql_test.go
│ │ │ └── mindsdbsql.go
│ │ ├── mongodb
│ │ │ ├── mongodbaggregate
│ │ │ │ ├── mongodbaggregate_test.go
│ │ │ │ └── mongodbaggregate.go
│ │ │ ├── mongodbdeletemany
│ │ │ │ ├── mongodbdeletemany_test.go
│ │ │ │ └── mongodbdeletemany.go
│ │ │ ├── mongodbdeleteone
│ │ │ │ ├── mongodbdeleteone_test.go
│ │ │ │ └── mongodbdeleteone.go
│ │ │ ├── mongodbfind
│ │ │ │ ├── mongodbfind_test.go
│ │ │ │ └── mongodbfind.go
│ │ │ ├── mongodbfindone
│ │ │ │ ├── mongodbfindone_test.go
│ │ │ │ └── mongodbfindone.go
│ │ │ ├── mongodbinsertmany
│ │ │ │ ├── mongodbinsertmany_test.go
│ │ │ │ └── mongodbinsertmany.go
│ │ │ ├── mongodbinsertone
│ │ │ │ ├── mongodbinsertone_test.go
│ │ │ │ └── mongodbinsertone.go
│ │ │ ├── mongodbupdatemany
│ │ │ │ ├── mongodbupdatemany_test.go
│ │ │ │ └── mongodbupdatemany.go
│ │ │ └── mongodbupdateone
│ │ │ ├── mongodbupdateone_test.go
│ │ │ └── mongodbupdateone.go
│ │ ├── mssql
│ │ │ ├── mssqlexecutesql
│ │ │ │ ├── mssqlexecutesql_test.go
│ │ │ │ └── mssqlexecutesql.go
│ │ │ ├── mssqllisttables
│ │ │ │ ├── mssqllisttables_test.go
│ │ │ │ └── mssqllisttables.go
│ │ │ └── mssqlsql
│ │ │ ├── mssqlsql_test.go
│ │ │ └── mssqlsql.go
│ │ ├── mysql
│ │ │ ├── mysqlcommon
│ │ │ │ └── mysqlcommon.go
│ │ │ ├── mysqlexecutesql
│ │ │ │ ├── mysqlexecutesql_test.go
│ │ │ │ └── mysqlexecutesql.go
│ │ │ ├── mysqllistactivequeries
│ │ │ │ ├── mysqllistactivequeries_test.go
│ │ │ │ └── mysqllistactivequeries.go
│ │ │ ├── mysqllisttablefragmentation
│ │ │ │ ├── mysqllisttablefragmentation_test.go
│ │ │ │ └── mysqllisttablefragmentation.go
│ │ │ ├── mysqllisttables
│ │ │ │ ├── mysqllisttables_test.go
│ │ │ │ └── mysqllisttables.go
│ │ │ ├── mysqllisttablesmissinguniqueindexes
│ │ │ │ ├── mysqllisttablesmissinguniqueindexes_test.go
│ │ │ │ └── mysqllisttablesmissinguniqueindexes.go
│ │ │ └── mysqlsql
│ │ │ ├── mysqlsql_test.go
│ │ │ └── mysqlsql.go
│ │ ├── neo4j
│ │ │ ├── neo4jcypher
│ │ │ │ ├── neo4jcypher_test.go
│ │ │ │ └── neo4jcypher.go
│ │ │ ├── neo4jexecutecypher
│ │ │ │ ├── classifier
│ │ │ │ │ ├── classifier_test.go
│ │ │ │ │ └── classifier.go
│ │ │ │ ├── neo4jexecutecypher_test.go
│ │ │ │ └── neo4jexecutecypher.go
│ │ │ └── neo4jschema
│ │ │ ├── cache
│ │ │ │ ├── cache_test.go
│ │ │ │ └── cache.go
│ │ │ ├── helpers
│ │ │ │ ├── helpers_test.go
│ │ │ │ └── helpers.go
│ │ │ ├── neo4jschema_test.go
│ │ │ ├── neo4jschema.go
│ │ │ └── types
│ │ │ └── types.go
│ │ ├── oceanbase
│ │ │ ├── oceanbaseexecutesql
│ │ │ │ ├── oceanbaseexecutesql_test.go
│ │ │ │ └── oceanbaseexecutesql.go
│ │ │ └── oceanbasesql
│ │ │ ├── oceanbasesql_test.go
│ │ │ └── oceanbasesql.go
│ │ ├── oracle
│ │ │ ├── oracleexecutesql
│ │ │ │ └── oracleexecutesql.go
│ │ │ └── oraclesql
│ │ │ └── oraclesql.go
│ │ ├── parameters_test.go
│ │ ├── parameters.go
│ │ ├── postgres
│ │ │ ├── postgresexecutesql
│ │ │ │ ├── postgresexecutesql_test.go
│ │ │ │ └── postgresexecutesql.go
│ │ │ ├── postgreslistactivequeries
│ │ │ │ ├── postgreslistactivequeries_test.go
│ │ │ │ └── postgreslistactivequeries.go
│ │ │ ├── postgreslistavailableextensions
│ │ │ │ ├── postgreslistavailableextensions_test.go
│ │ │ │ └── postgreslistavailableextensions.go
│ │ │ ├── postgreslistinstalledextensions
│ │ │ │ ├── postgreslistinstalledextensions_test.go
│ │ │ │ └── postgreslistinstalledextensions.go
│ │ │ ├── postgreslistschemas
│ │ │ │ ├── postgreslistschemas_test.go
│ │ │ │ └── postgreslistschemas.go
│ │ │ ├── postgreslisttables
│ │ │ │ ├── postgreslisttables_test.go
│ │ │ │ └── postgreslisttables.go
│ │ │ ├── postgreslistviews
│ │ │ │ ├── postgreslistviews_test.go
│ │ │ │ └── postgreslistviews.go
│ │ │ └── postgressql
│ │ │ ├── postgressql_test.go
│ │ │ └── postgressql.go
│ │ ├── redis
│ │ │ ├── redis_test.go
│ │ │ └── redis.go
│ │ ├── serverlessspark
│ │ │ ├── serverlesssparkcancelbatch
│ │ │ │ ├── serverlesssparkcancelbatch_test.go
│ │ │ │ └── serverlesssparkcancelbatch.go
│ │ │ ├── serverlesssparkgetbatch
│ │ │ │ ├── serverlesssparkgetbatch_test.go
│ │ │ │ └── serverlesssparkgetbatch.go
│ │ │ └── serverlesssparklistbatches
│ │ │ ├── serverlesssparklistbatches_test.go
│ │ │ └── serverlesssparklistbatches.go
│ │ ├── spanner
│ │ │ ├── spannerexecutesql
│ │ │ │ ├── spannerexecutesql_test.go
│ │ │ │ └── spannerexecutesql.go
│ │ │ ├── spannerlisttables
│ │ │ │ ├── spannerlisttables_test.go
│ │ │ │ └── spannerlisttables.go
│ │ │ └── spannersql
│ │ │ ├── spanner_test.go
│ │ │ └── spannersql.go
│ │ ├── sqlite
│ │ │ ├── sqliteexecutesql
│ │ │ │ ├── sqliteexecutesql_test.go
│ │ │ │ └── sqliteexecutesql.go
│ │ │ └── sqlitesql
│ │ │ ├── sqlitesql_test.go
│ │ │ └── sqlitesql.go
│ │ ├── tidb
│ │ │ ├── tidbexecutesql
│ │ │ │ ├── tidbexecutesql_test.go
│ │ │ │ └── tidbexecutesql.go
│ │ │ └── tidbsql
│ │ │ ├── tidbsql_test.go
│ │ │ └── tidbsql.go
│ │ ├── tools_test.go
│ │ ├── tools.go
│ │ ├── toolsets.go
│ │ ├── trino
│ │ │ ├── trinoexecutesql
│ │ │ │ ├── trinoexecutesql_test.go
│ │ │ │ └── trinoexecutesql.go
│ │ │ └── trinosql
│ │ │ ├── trinosql_test.go
│ │ │ └── trinosql.go
│ │ ├── utility
│ │ │ └── wait
│ │ │ ├── wait_test.go
│ │ │ └── wait.go
│ │ ├── valkey
│ │ │ ├── valkey_test.go
│ │ │ └── valkey.go
│ │ └── yugabytedbsql
│ │ ├── yugabytedbsql_test.go
│ │ └── yugabytedbsql.go
│ └── util
│ ├── orderedmap
│ │ ├── orderedmap_test.go
│ │ └── orderedmap.go
│ └── util.go
├── LICENSE
├── logo.png
├── main.go
├── MCP-TOOLBOX-EXTENSION.md
├── README.md
└── tests
├── alloydb
│ ├── alloydb_integration_test.go
│ └── alloydb_wait_for_operation_test.go
├── alloydbainl
│ └── alloydb_ai_nl_integration_test.go
├── alloydbpg
│ └── alloydb_pg_integration_test.go
├── auth.go
├── bigquery
│ └── bigquery_integration_test.go
├── bigtable
│ └── bigtable_integration_test.go
├── cassandra
│ └── cassandra_integration_test.go
├── clickhouse
│ └── clickhouse_integration_test.go
├── cloudmonitoring
│ └── cloud_monitoring_integration_test.go
├── cloudsql
│ ├── cloud_sql_create_database_test.go
│ ├── cloud_sql_create_users_test.go
│ ├── cloud_sql_get_instances_test.go
│ ├── cloud_sql_list_databases_test.go
│ ├── cloudsql_list_instances_test.go
│ └── cloudsql_wait_for_operation_test.go
├── cloudsqlmssql
│ ├── cloud_sql_mssql_create_instance_integration_test.go
│ └── cloud_sql_mssql_integration_test.go
├── cloudsqlmysql
│ ├── cloud_sql_mysql_create_instance_integration_test.go
│ └── cloud_sql_mysql_integration_test.go
├── cloudsqlpg
│ ├── cloud_sql_pg_create_instances_test.go
│ └── cloud_sql_pg_integration_test.go
├── common.go
├── couchbase
│ └── couchbase_integration_test.go
├── dataform
│ └── dataform_integration_test.go
├── dataplex
│ └── dataplex_integration_test.go
├── dgraph
│ └── dgraph_integration_test.go
├── elasticsearch
│ └── elasticsearch_integration_test.go
├── firebird
│ └── firebird_integration_test.go
├── firestore
│ └── firestore_integration_test.go
├── http
│ └── http_integration_test.go
├── looker
│ └── looker_integration_test.go
├── mindsdb
│ └── mindsdb_integration_test.go
├── mongodb
│ └── mongodb_integration_test.go
├── mssql
│ └── mssql_integration_test.go
├── mysql
│ └── mysql_integration_test.go
├── neo4j
│ └── neo4j_integration_test.go
├── oceanbase
│ └── oceanbase_integration_test.go
├── option.go
├── oracle
│ └── oracle_integration_test.go
├── postgres
│ └── postgres_integration_test.go
├── redis
│ └── redis_test.go
├── server.go
├── serverlessspark
│ └── serverless_spark_integration_test.go
├── source.go
├── spanner
│ └── spanner_integration_test.go
├── sqlite
│ └── sqlite_integration_test.go
├── tidb
│ └── tidb_integration_test.go
├── tool.go
├── trino
│ └── trino_integration_test.go
├── utility
│ └── wait_integration_test.go
├── valkey
│ └── valkey_test.go
└── yugabytedb
└── yugabytedb_integration_test.go
```
# Files
--------------------------------------------------------------------------------
/tests/tool.go:
--------------------------------------------------------------------------------
```go
1 | // Copyright 2025 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package tests
16 |
17 | import (
18 | "bytes"
19 | "context"
20 | "database/sql"
21 | "encoding/json"
22 | "fmt"
23 | "io"
24 | "net/http"
25 | "reflect"
26 | "sort"
27 | "strings"
28 | "sync"
29 | "testing"
30 | "time"
31 |
32 | "github.com/google/go-cmp/cmp"
33 | "github.com/google/go-cmp/cmp/cmpopts"
34 | "github.com/google/uuid"
35 | "github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
36 | "github.com/googleapis/genai-toolbox/internal/sources"
37 | "github.com/jackc/pgx/v5/pgxpool"
38 | )
39 |
40 | // RunToolGet runs the tool get endpoint
41 | func RunToolGetTest(t *testing.T) {
42 | // Test tool get endpoint
43 | tcs := []struct {
44 | name string
45 | api string
46 | want map[string]any
47 | }{
48 | {
49 | name: "get my-simple-tool",
50 | api: "http://127.0.0.1:5000/api/tool/my-simple-tool/",
51 | want: map[string]any{
52 | "my-simple-tool": map[string]any{
53 | "description": "Simple tool to test end to end functionality.",
54 | "parameters": []any{},
55 | "authRequired": []any{},
56 | },
57 | },
58 | },
59 | }
60 | for _, tc := range tcs {
61 | t.Run(tc.name, func(t *testing.T) {
62 | resp, err := http.Get(tc.api)
63 | if err != nil {
64 | t.Fatalf("error when sending a request: %s", err)
65 | }
66 | defer resp.Body.Close()
67 | if resp.StatusCode != 200 {
68 | t.Fatalf("response status code is not 200")
69 | }
70 |
71 | var body map[string]interface{}
72 | err = json.NewDecoder(resp.Body).Decode(&body)
73 | if err != nil {
74 | t.Fatalf("error parsing response body")
75 | }
76 |
77 | got, ok := body["tools"]
78 | if !ok {
79 | t.Fatalf("unable to find tools in response body")
80 | }
81 | if !reflect.DeepEqual(got, tc.want) {
82 | t.Fatalf("got %q, want %q", got, tc.want)
83 | }
84 | })
85 | }
86 | }
87 |
88 | func RunToolGetTestByName(t *testing.T, name string, want map[string]any) {
89 | // Test tool get endpoint
90 | tcs := []struct {
91 | name string
92 | api string
93 | want map[string]any
94 | }{
95 | {
96 | name: fmt.Sprintf("get %s", name),
97 | api: fmt.Sprintf("http://127.0.0.1:5000/api/tool/%s/", name),
98 | want: want,
99 | },
100 | }
101 | for _, tc := range tcs {
102 | t.Run(tc.name, func(t *testing.T) {
103 | resp, err := http.Get(tc.api)
104 | if err != nil {
105 | t.Fatalf("error when sending a request: %s", err)
106 | }
107 | defer resp.Body.Close()
108 | if resp.StatusCode != 200 {
109 | t.Fatalf("response status code is not 200")
110 | }
111 |
112 | var body map[string]interface{}
113 | err = json.NewDecoder(resp.Body).Decode(&body)
114 | if err != nil {
115 | t.Fatalf("error parsing response body")
116 | }
117 |
118 | got, ok := body["tools"]
119 | if !ok {
120 | t.Fatalf("unable to find tools in response body")
121 | }
122 | if !reflect.DeepEqual(got, tc.want) {
123 | t.Fatalf("got %q, want %q", got, tc.want)
124 | }
125 | })
126 | }
127 | }
128 |
129 | // RunToolInvokeSimpleTest runs the tool invoke endpoint with no parameters
130 | func RunToolInvokeSimpleTest(t *testing.T, name string, simpleWant string) {
131 | // Test tool invoke endpoint
132 | invokeTcs := []struct {
133 | name string
134 | api string
135 | requestHeader map[string]string
136 | requestBody io.Reader
137 | want string
138 | isErr bool
139 | }{
140 | {
141 | name: fmt.Sprintf("invoke %s", name),
142 | api: fmt.Sprintf("http://127.0.0.1:5000/api/tool/%s/invoke", name),
143 | requestHeader: map[string]string{},
144 | requestBody: bytes.NewBuffer([]byte(`{}`)),
145 | want: simpleWant,
146 | isErr: false,
147 | },
148 | }
149 | for _, tc := range invokeTcs {
150 | t.Run(tc.name, func(t *testing.T) {
151 | // Send Tool invocation request
152 | req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
153 | if err != nil {
154 | t.Fatalf("unable to create request: %s", err)
155 | }
156 | req.Header.Add("Content-type", "application/json")
157 | for k, v := range tc.requestHeader {
158 | req.Header.Add(k, v)
159 | }
160 | resp, err := http.DefaultClient.Do(req)
161 | if err != nil {
162 | t.Fatalf("unable to send request: %s", err)
163 | }
164 | defer resp.Body.Close()
165 |
166 | if resp.StatusCode != http.StatusOK {
167 | if tc.isErr {
168 | return
169 | }
170 | bodyBytes, _ := io.ReadAll(resp.Body)
171 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
172 | }
173 |
174 | // Check response body
175 | var body map[string]interface{}
176 | err = json.NewDecoder(resp.Body).Decode(&body)
177 | if err != nil {
178 | t.Fatalf("error parsing response body")
179 | }
180 |
181 | got, ok := body["result"].(string)
182 | if !ok {
183 | t.Fatalf("unable to find result in response body")
184 | }
185 |
186 | if !strings.Contains(got, tc.want) {
187 | t.Fatalf("unexpected value: got %q, want %q", got, tc.want)
188 | }
189 | })
190 | }
191 | }
192 |
193 | func RunToolInvokeParametersTest(t *testing.T, name string, params []byte, simpleWant string) {
194 | // Test tool invoke endpoint
195 | invokeTcs := []struct {
196 | name string
197 | api string
198 | requestHeader map[string]string
199 | requestBody io.Reader
200 | want string
201 | isErr bool
202 | }{
203 | {
204 | name: fmt.Sprintf("invoke %s", name),
205 | api: fmt.Sprintf("http://127.0.0.1:5000/api/tool/%s/invoke", name),
206 | requestHeader: map[string]string{},
207 | requestBody: bytes.NewBuffer(params),
208 | want: simpleWant,
209 | isErr: false,
210 | },
211 | }
212 | for _, tc := range invokeTcs {
213 | t.Run(tc.name, func(t *testing.T) {
214 | // Send Tool invocation request
215 | req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
216 | if err != nil {
217 | t.Fatalf("unable to create request: %s", err)
218 | }
219 | req.Header.Add("Content-type", "application/json")
220 | for k, v := range tc.requestHeader {
221 | req.Header.Add(k, v)
222 | }
223 | resp, err := http.DefaultClient.Do(req)
224 | if err != nil {
225 | t.Fatalf("unable to send request: %s", err)
226 | }
227 | defer resp.Body.Close()
228 |
229 | if resp.StatusCode != http.StatusOK {
230 | if tc.isErr {
231 | return
232 | }
233 | bodyBytes, _ := io.ReadAll(resp.Body)
234 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
235 | }
236 |
237 | // Check response body
238 | var body map[string]interface{}
239 | err = json.NewDecoder(resp.Body).Decode(&body)
240 | if err != nil {
241 | t.Fatalf("error parsing response body")
242 | }
243 |
244 | got, ok := body["result"].(string)
245 | if !ok {
246 | t.Fatalf("unable to find result in response body")
247 | }
248 |
249 | if !strings.Contains(got, tc.want) {
250 | t.Fatalf("unexpected value: got %q, want %q", got, tc.want)
251 | }
252 | })
253 | }
254 | }
255 |
256 | // RunToolInvoke runs the tool invoke endpoint
257 | func RunToolInvokeTest(t *testing.T, select1Want string, options ...InvokeTestOption) {
258 | // Resolve options
259 | // Default values for InvokeTestConfig
260 | configs := &InvokeTestConfig{
261 | myToolId3NameAliceWant: "[{\"id\":1,\"name\":\"Alice\"},{\"id\":3,\"name\":\"Sid\"}]",
262 | myToolById4Want: "[{\"id\":4,\"name\":null}]",
263 | myArrayToolWant: "[{\"id\":1,\"name\":\"Alice\"},{\"id\":3,\"name\":\"Sid\"}]",
264 | nullWant: "null",
265 | supportOptionalNullParam: true,
266 | supportArrayParam: true,
267 | supportClientAuth: false,
268 | supportSelect1Want: true,
269 | supportSelect1Auth: true,
270 | }
271 |
272 | // Apply provided options
273 | for _, option := range options {
274 | option(configs)
275 | }
276 |
277 | // Get ID token
278 | idToken, err := GetGoogleIdToken(ClientId)
279 | if err != nil {
280 | t.Fatalf("error getting Google ID token: %s", err)
281 | }
282 |
283 | // Get access token
284 | accessToken, err := sources.GetIAMAccessToken(t.Context())
285 | if err != nil {
286 | t.Fatalf("error getting access token from ADC: %s", err)
287 | }
288 | accessToken = "Bearer " + accessToken
289 |
290 | // Test tool invoke endpoint
291 | invokeTcs := []struct {
292 | name string
293 | api string
294 | enabled bool
295 | requestHeader map[string]string
296 | requestBody io.Reader
297 | wantStatusCode int
298 | wantBody string
299 | }{
300 | {
301 | name: "invoke my-simple-tool",
302 | api: "http://127.0.0.1:5000/api/tool/my-simple-tool/invoke",
303 | enabled: configs.supportSelect1Want,
304 | requestHeader: map[string]string{},
305 | requestBody: bytes.NewBuffer([]byte(`{}`)),
306 | wantBody: select1Want,
307 | wantStatusCode: http.StatusOK,
308 | },
309 | {
310 | name: "invoke my-tool",
311 | api: "http://127.0.0.1:5000/api/tool/my-tool/invoke",
312 | enabled: true,
313 | requestHeader: map[string]string{},
314 | requestBody: bytes.NewBuffer([]byte(`{"id": 3, "name": "Alice"}`)),
315 | wantBody: configs.myToolId3NameAliceWant,
316 | wantStatusCode: http.StatusOK,
317 | },
318 | {
319 | name: "invoke my-tool-by-id with nil response",
320 | api: "http://127.0.0.1:5000/api/tool/my-tool-by-id/invoke",
321 | enabled: true,
322 | requestHeader: map[string]string{},
323 | requestBody: bytes.NewBuffer([]byte(`{"id": 4}`)),
324 | wantBody: configs.myToolById4Want,
325 | wantStatusCode: http.StatusOK,
326 | },
327 | {
328 | name: "invoke my-tool-by-name with nil response",
329 | api: "http://127.0.0.1:5000/api/tool/my-tool-by-name/invoke",
330 | enabled: configs.supportOptionalNullParam,
331 | requestHeader: map[string]string{},
332 | requestBody: bytes.NewBuffer([]byte(`{}`)),
333 | wantBody: configs.nullWant,
334 | wantStatusCode: http.StatusOK,
335 | },
336 | {
337 | name: "Invoke my-tool without parameters",
338 | api: "http://127.0.0.1:5000/api/tool/my-tool/invoke",
339 | enabled: true,
340 | requestHeader: map[string]string{},
341 | requestBody: bytes.NewBuffer([]byte(`{}`)),
342 | wantBody: "",
343 | wantStatusCode: http.StatusBadRequest,
344 | },
345 | {
346 | name: "Invoke my-tool with insufficient parameters",
347 | api: "http://127.0.0.1:5000/api/tool/my-tool/invoke",
348 | enabled: true,
349 | requestHeader: map[string]string{},
350 | requestBody: bytes.NewBuffer([]byte(`{"id": 1}`)),
351 | wantBody: "",
352 | wantStatusCode: http.StatusBadRequest,
353 | },
354 | {
355 | name: "invoke my-array-tool",
356 | api: "http://127.0.0.1:5000/api/tool/my-array-tool/invoke",
357 | enabled: configs.supportArrayParam,
358 | requestHeader: map[string]string{},
359 | requestBody: bytes.NewBuffer([]byte(`{"idArray": [1,2,3], "nameArray": ["Alice", "Sid", "RandomName"], "cmdArray": ["HGETALL", "row3"]}`)),
360 | wantBody: configs.myArrayToolWant,
361 | wantStatusCode: http.StatusOK,
362 | },
363 | {
364 | name: "Invoke my-auth-tool with auth token",
365 | api: "http://127.0.0.1:5000/api/tool/my-auth-tool/invoke",
366 | enabled: configs.supportSelect1Auth,
367 | requestHeader: map[string]string{"my-google-auth_token": idToken},
368 | requestBody: bytes.NewBuffer([]byte(`{}`)),
369 | wantBody: configs.myAuthToolWant,
370 | wantStatusCode: http.StatusOK,
371 | },
372 | {
373 | name: "Invoke my-auth-tool with invalid auth token",
374 | api: "http://127.0.0.1:5000/api/tool/my-auth-tool/invoke",
375 | enabled: configs.supportSelect1Auth,
376 | requestHeader: map[string]string{"my-google-auth_token": "INVALID_TOKEN"},
377 | requestBody: bytes.NewBuffer([]byte(`{}`)),
378 | wantBody: "",
379 | wantStatusCode: http.StatusUnauthorized,
380 | },
381 | {
382 | name: "Invoke my-auth-tool without auth token",
383 | api: "http://127.0.0.1:5000/api/tool/my-auth-tool/invoke",
384 | enabled: true,
385 | requestHeader: map[string]string{},
386 | requestBody: bytes.NewBuffer([]byte(`{}`)),
387 | wantBody: "",
388 | wantStatusCode: http.StatusUnauthorized,
389 | },
390 | {
391 | name: "Invoke my-auth-required-tool with auth token",
392 | api: "http://127.0.0.1:5000/api/tool/my-auth-required-tool/invoke",
393 | enabled: configs.supportSelect1Auth,
394 | requestHeader: map[string]string{"my-google-auth_token": idToken},
395 | requestBody: bytes.NewBuffer([]byte(`{}`)),
396 | wantBody: select1Want,
397 | wantStatusCode: http.StatusOK,
398 | },
399 | {
400 | name: "Invoke my-auth-required-tool with invalid auth token",
401 | api: "http://127.0.0.1:5000/api/tool/my-auth-required-tool/invoke",
402 | enabled: true,
403 | requestHeader: map[string]string{"my-google-auth_token": "INVALID_TOKEN"},
404 | requestBody: bytes.NewBuffer([]byte(`{}`)),
405 | wantBody: "",
406 | wantStatusCode: http.StatusUnauthorized,
407 | },
408 | {
409 | name: "Invoke my-auth-required-tool without auth token",
410 | api: "http://127.0.0.1:5000/api/tool/my-auth-tool/invoke",
411 | enabled: true,
412 | requestHeader: map[string]string{},
413 | requestBody: bytes.NewBuffer([]byte(`{}`)),
414 | wantBody: "",
415 | wantStatusCode: http.StatusUnauthorized,
416 | },
417 | {
418 | name: "Invoke my-client-auth-tool with auth token",
419 | api: "http://127.0.0.1:5000/api/tool/my-client-auth-tool/invoke",
420 | enabled: configs.supportClientAuth,
421 | requestHeader: map[string]string{"Authorization": accessToken},
422 | requestBody: bytes.NewBuffer([]byte(`{}`)),
423 | wantBody: select1Want,
424 | wantStatusCode: http.StatusOK,
425 | },
426 | {
427 | name: "Invoke my-client-auth-tool without auth token",
428 | api: "http://127.0.0.1:5000/api/tool/my-client-auth-tool/invoke",
429 | enabled: configs.supportClientAuth,
430 | requestHeader: map[string]string{},
431 | requestBody: bytes.NewBuffer([]byte(`{}`)),
432 | wantStatusCode: http.StatusUnauthorized,
433 | },
434 | {
435 |
436 | name: "Invoke my-client-auth-tool with invalid auth token",
437 | api: "http://127.0.0.1:5000/api/tool/my-client-auth-tool/invoke",
438 | enabled: configs.supportClientAuth,
439 | requestHeader: map[string]string{"Authorization": "Bearer invalid-token"},
440 | requestBody: bytes.NewBuffer([]byte(`{}`)),
441 | wantStatusCode: http.StatusUnauthorized,
442 | },
443 | }
444 | for _, tc := range invokeTcs {
445 | t.Run(tc.name, func(t *testing.T) {
446 | if !tc.enabled {
447 | return
448 | }
449 | // Send Tool invocation request
450 | req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
451 | if err != nil {
452 | t.Fatalf("unable to create request: %s", err)
453 | }
454 | req.Header.Add("Content-type", "application/json")
455 | // Add headers
456 | for k, v := range tc.requestHeader {
457 | req.Header.Add(k, v)
458 | }
459 | resp, err := http.DefaultClient.Do(req)
460 | if err != nil {
461 | t.Fatalf("unable to send request: %s", err)
462 | }
463 | defer resp.Body.Close()
464 |
465 | // Check status code
466 | if resp.StatusCode != tc.wantStatusCode {
467 | body, _ := io.ReadAll(resp.Body)
468 | t.Errorf("StatusCode mismatch: got %d, want %d. Response body: %s", resp.StatusCode, tc.wantStatusCode, string(body))
469 | }
470 |
471 | // skip response body check
472 | if tc.wantBody == "" {
473 | return
474 | }
475 |
476 | // Check response body
477 | var body map[string]interface{}
478 | err = json.NewDecoder(resp.Body).Decode(&body)
479 | if err != nil {
480 | t.Fatalf("error parsing response body: %s", err)
481 | }
482 |
483 | got, ok := body["result"].(string)
484 | if !ok {
485 | t.Fatalf("unable to find result in response body")
486 | }
487 |
488 | if got != tc.wantBody {
489 | t.Fatalf("unexpected value: got %q, want %q", got, tc.wantBody)
490 | }
491 | })
492 | }
493 | }
494 |
495 | // RunToolInvokeWithTemplateParameters runs tool invoke test cases with template parameters.
496 | func RunToolInvokeWithTemplateParameters(t *testing.T, tableName string, options ...TemplateParamOption) {
497 | // Resolve options
498 | // Default values for TemplateParameterTestConfig
499 | configs := &TemplateParameterTestConfig{
500 | ddlWant: "null",
501 | selectAllWant: "[{\"age\":21,\"id\":1,\"name\":\"Alex\"},{\"age\":100,\"id\":2,\"name\":\"Alice\"}]",
502 | selectId1Want: "[{\"age\":21,\"id\":1,\"name\":\"Alex\"}]",
503 | selectNameWant: "[{\"age\":21,\"id\":1,\"name\":\"Alex\"}]",
504 | selectEmptyWant: "null",
505 | insert1Want: "null",
506 |
507 | nameFieldArray: `["name"]`,
508 | nameColFilter: "name",
509 | createColArray: `["id INT","name VARCHAR(20)","age INT"]`,
510 |
511 | supportDdl: true,
512 | supportInsert: true,
513 | }
514 |
515 | // Apply provided options
516 | for _, option := range options {
517 | option(configs)
518 | }
519 |
520 | selectOnlyNamesWant := "[{\"name\":\"Alex\"},{\"name\":\"Alice\"}]"
521 |
522 | // Test tool invoke endpoint
523 | invokeTcs := []struct {
524 | name string
525 | enabled bool
526 | ddl bool
527 | insert bool
528 | api string
529 | requestHeader map[string]string
530 | requestBody io.Reader
531 | want string
532 | isErr bool
533 | }{
534 | {
535 | name: "invoke create-table-templateParams-tool",
536 | ddl: true,
537 | api: "http://127.0.0.1:5000/api/tool/create-table-templateParams-tool/invoke",
538 | requestHeader: map[string]string{},
539 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"tableName": "%s", "columns":%s}`, tableName, configs.createColArray))),
540 | want: configs.ddlWant,
541 | isErr: false,
542 | },
543 | {
544 | name: "invoke insert-table-templateParams-tool",
545 | insert: true,
546 | api: "http://127.0.0.1:5000/api/tool/insert-table-templateParams-tool/invoke",
547 | requestHeader: map[string]string{},
548 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"tableName": "%s", "columns":["id","name","age"], "values":"1, 'Alex', 21"}`, tableName))),
549 | want: configs.insert1Want,
550 | isErr: false,
551 | },
552 | {
553 | name: "invoke insert-table-templateParams-tool",
554 | insert: true,
555 | api: "http://127.0.0.1:5000/api/tool/insert-table-templateParams-tool/invoke",
556 | requestHeader: map[string]string{},
557 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"tableName": "%s", "columns":["id","name","age"], "values":"2, 'Alice', 100"}`, tableName))),
558 | want: configs.insert1Want,
559 | isErr: false,
560 | },
561 | {
562 | name: "invoke select-templateParams-tool",
563 | api: "http://127.0.0.1:5000/api/tool/select-templateParams-tool/invoke",
564 | requestHeader: map[string]string{},
565 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"tableName": "%s"}`, tableName))),
566 | want: configs.selectAllWant,
567 | isErr: false,
568 | },
569 | {
570 | name: "invoke select-templateParams-combined-tool",
571 | api: "http://127.0.0.1:5000/api/tool/select-templateParams-combined-tool/invoke",
572 | requestHeader: map[string]string{},
573 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"id": 1, "tableName": "%s"}`, tableName))),
574 | want: configs.selectId1Want,
575 | isErr: false,
576 | },
577 | {
578 | name: "invoke select-templateParams-combined-tool with no results",
579 | api: "http://127.0.0.1:5000/api/tool/select-templateParams-combined-tool/invoke",
580 | requestHeader: map[string]string{},
581 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"id": 999, "tableName": "%s"}`, tableName))),
582 | want: configs.selectEmptyWant,
583 | isErr: false,
584 | },
585 | {
586 | name: "invoke select-fields-templateParams-tool",
587 | enabled: configs.supportSelectFields,
588 | api: "http://127.0.0.1:5000/api/tool/select-fields-templateParams-tool/invoke",
589 | requestHeader: map[string]string{},
590 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"tableName": "%s", "fields":%s}`, tableName, configs.nameFieldArray))),
591 | want: selectOnlyNamesWant,
592 | isErr: false,
593 | },
594 | {
595 | name: "invoke select-filter-templateParams-combined-tool",
596 | api: "http://127.0.0.1:5000/api/tool/select-filter-templateParams-combined-tool/invoke",
597 | requestHeader: map[string]string{},
598 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"name": "Alex", "tableName": "%s", "columnFilter": "%s"}`, tableName, configs.nameColFilter))),
599 | want: configs.selectNameWant,
600 | isErr: false,
601 | },
602 | {
603 | name: "invoke drop-table-templateParams-tool",
604 | ddl: true,
605 | api: "http://127.0.0.1:5000/api/tool/drop-table-templateParams-tool/invoke",
606 | requestHeader: map[string]string{},
607 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"tableName": "%s"}`, tableName))),
608 | want: configs.ddlWant,
609 | isErr: false,
610 | },
611 | }
612 | for _, tc := range invokeTcs {
613 | t.Run(tc.name, func(t *testing.T) {
614 | if !tc.enabled {
615 | return
616 | }
617 | // if test case is DDL and source support ddl test cases
618 | ddlAllow := !tc.ddl || (tc.ddl && configs.supportDdl)
619 | // if test case is insert statement and source support insert test cases
620 | insertAllow := !tc.insert || (tc.insert && configs.supportInsert)
621 | if ddlAllow && insertAllow {
622 | // Send Tool invocation request
623 | req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
624 | if err != nil {
625 | t.Fatalf("unable to create request: %s", err)
626 | }
627 | req.Header.Add("Content-type", "application/json")
628 | for k, v := range tc.requestHeader {
629 | req.Header.Add(k, v)
630 | }
631 |
632 | resp, err := http.DefaultClient.Do(req)
633 | if err != nil {
634 | t.Fatalf("unable to send request: %s", err)
635 | }
636 | defer resp.Body.Close()
637 |
638 | if resp.StatusCode != http.StatusOK {
639 | if tc.isErr {
640 | return
641 | }
642 | bodyBytes, _ := io.ReadAll(resp.Body)
643 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
644 | }
645 |
646 | // Check response body
647 | var body map[string]interface{}
648 | err = json.NewDecoder(resp.Body).Decode(&body)
649 | if err != nil {
650 | t.Fatalf("error parsing response body")
651 | }
652 |
653 | got, ok := body["result"].(string)
654 | if !ok {
655 | t.Fatalf("unable to find result in response body")
656 | }
657 |
658 | if got != tc.want {
659 | t.Fatalf("unexpected value: got %q, want %q", got, tc.want)
660 | }
661 | }
662 | })
663 | }
664 | }
665 |
666 | func RunExecuteSqlToolInvokeTest(t *testing.T, createTableStatement, select1Want string, options ...ExecuteSqlOption) {
667 | // Resolve options
668 | // Default values for ExecuteSqlTestConfig
669 | configs := &ExecuteSqlTestConfig{
670 | select1Statement: `"SELECT 1"`,
671 | }
672 |
673 | // Apply provided options
674 | for _, option := range options {
675 | option(configs)
676 | }
677 |
678 | // Get ID token
679 | idToken, err := GetGoogleIdToken(ClientId)
680 | if err != nil {
681 | t.Fatalf("error getting Google ID token: %s", err)
682 | }
683 |
684 | // Test tool invoke endpoint
685 | invokeTcs := []struct {
686 | name string
687 | api string
688 | requestHeader map[string]string
689 | requestBody io.Reader
690 | want string
691 | isErr bool
692 | }{
693 | {
694 | name: "invoke my-exec-sql-tool",
695 | api: "http://127.0.0.1:5000/api/tool/my-exec-sql-tool/invoke",
696 | requestHeader: map[string]string{},
697 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"sql": %s}`, configs.select1Statement))),
698 | want: select1Want,
699 | isErr: false,
700 | },
701 | {
702 | name: "invoke my-exec-sql-tool create table",
703 | api: "http://127.0.0.1:5000/api/tool/my-exec-sql-tool/invoke",
704 | requestHeader: map[string]string{},
705 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"sql": %s}`, createTableStatement))),
706 | want: "null",
707 | isErr: false,
708 | },
709 | {
710 | name: "invoke my-exec-sql-tool select table",
711 | api: "http://127.0.0.1:5000/api/tool/my-exec-sql-tool/invoke",
712 | requestHeader: map[string]string{},
713 | requestBody: bytes.NewBuffer([]byte(`{"sql":"SELECT * FROM t"}`)),
714 | want: "null",
715 | isErr: false,
716 | },
717 | {
718 | name: "invoke my-exec-sql-tool drop table",
719 | api: "http://127.0.0.1:5000/api/tool/my-exec-sql-tool/invoke",
720 | requestHeader: map[string]string{},
721 | requestBody: bytes.NewBuffer([]byte(`{"sql":"DROP TABLE t"}`)),
722 | want: "null",
723 | isErr: false,
724 | },
725 | {
726 | name: "invoke my-exec-sql-tool without body",
727 | api: "http://127.0.0.1:5000/api/tool/my-exec-sql-tool/invoke",
728 | requestHeader: map[string]string{},
729 | requestBody: bytes.NewBuffer([]byte(`{}`)),
730 | isErr: true,
731 | },
732 | {
733 | name: "Invoke my-auth-exec-sql-tool with auth token",
734 | api: "http://127.0.0.1:5000/api/tool/my-auth-exec-sql-tool/invoke",
735 | requestHeader: map[string]string{"my-google-auth_token": idToken},
736 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"sql": %s}`, configs.select1Statement))),
737 | isErr: false,
738 | want: select1Want,
739 | },
740 | {
741 | name: "Invoke my-auth-exec-sql-tool with invalid auth token",
742 | api: "http://127.0.0.1:5000/api/tool/my-auth-exec-sql-tool/invoke",
743 | requestHeader: map[string]string{"my-google-auth_token": "INVALID_TOKEN"},
744 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"sql": %s}`, configs.select1Statement))),
745 | isErr: true,
746 | },
747 | {
748 | name: "Invoke my-auth-exec-sql-tool without auth token",
749 | api: "http://127.0.0.1:5000/api/tool/my-auth-exec-sql-tool/invoke",
750 | requestHeader: map[string]string{},
751 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"sql": %s}`, configs.select1Statement))),
752 | isErr: true,
753 | },
754 | {
755 | name: "invoke my-exec-sql-tool with invalid SELECT SQL",
756 | api: "http://127.0.0.1:5000/api/tool/my-exec-sql-tool/invoke",
757 | requestHeader: map[string]string{},
758 | requestBody: bytes.NewBuffer([]byte(`{"sql":"SELECT * FROM non_existent_table"}`)),
759 | isErr: true,
760 | },
761 | {
762 | name: "invoke my-exec-sql-tool with invalid ALTER SQL",
763 | api: "http://127.0.0.1:5000/api/tool/my-exec-sql-tool/invoke",
764 | requestHeader: map[string]string{},
765 | requestBody: bytes.NewBuffer([]byte(`{"sql":"ALTER TALE t ALTER COLUMN id DROP NOT NULL"}`)),
766 | isErr: true,
767 | },
768 | }
769 | for _, tc := range invokeTcs {
770 | t.Run(tc.name, func(t *testing.T) {
771 | // Send Tool invocation request
772 | req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
773 | if err != nil {
774 | t.Fatalf("unable to create request: %s", err)
775 | }
776 | req.Header.Add("Content-type", "application/json")
777 | for k, v := range tc.requestHeader {
778 | req.Header.Add(k, v)
779 | }
780 | resp, err := http.DefaultClient.Do(req)
781 | if err != nil {
782 | t.Fatalf("unable to send request: %s", err)
783 | }
784 | defer resp.Body.Close()
785 |
786 | if resp.StatusCode != http.StatusOK {
787 | if tc.isErr {
788 | return
789 | }
790 | bodyBytes, _ := io.ReadAll(resp.Body)
791 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
792 | }
793 |
794 | // Check response body
795 | var body map[string]interface{}
796 | err = json.NewDecoder(resp.Body).Decode(&body)
797 | if err != nil {
798 | t.Fatalf("error parsing response body")
799 | }
800 |
801 | got, ok := body["result"].(string)
802 | if !ok {
803 | t.Fatalf("unable to find result in response body")
804 | }
805 |
806 | if got != tc.want {
807 | t.Fatalf("unexpected value: got %q, want %q", got, tc.want)
808 | }
809 | })
810 | }
811 | }
812 |
813 | // RunInitialize runs the initialize lifecycle for mcp to set up client-server connection
814 | func RunInitialize(t *testing.T, protocolVersion string) string {
815 | url := "http://127.0.0.1:5000/mcp"
816 |
817 | initializeRequestBody := map[string]any{
818 | "jsonrpc": "2.0",
819 | "id": "mcp-initialize",
820 | "method": "initialize",
821 | "params": map[string]any{
822 | "protocolVersion": protocolVersion,
823 | },
824 | }
825 | reqMarshal, err := json.Marshal(initializeRequestBody)
826 | if err != nil {
827 | t.Fatalf("unexpected error during marshaling of body")
828 | }
829 |
830 | resp, _ := RunRequest(t, http.MethodPost, url, bytes.NewBuffer(reqMarshal), nil)
831 | if resp.StatusCode != 200 {
832 | t.Fatalf("response status code is not 200")
833 | }
834 |
835 | if contentType := resp.Header.Get("Content-type"); contentType != "application/json" {
836 | t.Fatalf("unexpected content-type header: want %s, got %s", "application/json", contentType)
837 | }
838 |
839 | sessionId := resp.Header.Get("Mcp-Session-Id")
840 |
841 | header := map[string]string{}
842 | if sessionId != "" {
843 | header["Mcp-Session-Id"] = sessionId
844 | }
845 |
846 | initializeNotificationBody := map[string]any{
847 | "jsonrpc": "2.0",
848 | "method": "notifications/initialized",
849 | }
850 | notiMarshal, err := json.Marshal(initializeNotificationBody)
851 | if err != nil {
852 | t.Fatalf("unexpected error during marshaling of notifications body")
853 | }
854 |
855 | _, _ = RunRequest(t, http.MethodPost, url, bytes.NewBuffer(notiMarshal), header)
856 | return sessionId
857 | }
858 |
859 | // RunMCPToolCallMethod runs the tool/call for mcp endpoint
860 | func RunMCPToolCallMethod(t *testing.T, myFailToolWant, select1Want string, options ...McpTestOption) {
861 | // Resolve options
862 | // Default values for MCPTestConfig
863 | configs := &MCPTestConfig{
864 | myToolId3NameAliceWant: `{"jsonrpc":"2.0","id":"my-tool","result":{"content":[{"type":"text","text":"{\"id\":1,\"name\":\"Alice\"}"},{"type":"text","text":"{\"id\":3,\"name\":\"Sid\"}"}]}}`,
865 | supportClientAuth: false,
866 | supportSelect1Auth: true,
867 | }
868 |
869 | // Apply provided options
870 | for _, option := range options {
871 | option(configs)
872 | }
873 |
874 | sessionId := RunInitialize(t, "2024-11-05")
875 |
876 | // Get access token
877 | accessToken, err := sources.GetIAMAccessToken(t.Context())
878 | if err != nil {
879 | t.Fatalf("error getting access token from ADC: %s", err)
880 | }
881 | accessToken = "Bearer " + accessToken
882 |
883 | idToken, err := GetGoogleIdToken(ClientId)
884 | if err != nil {
885 | t.Fatalf("error getting Google ID token: %s", err)
886 | }
887 |
888 | // Test tool invoke endpoint
889 | invokeTcs := []struct {
890 | name string
891 | api string
892 | enabled bool // switch to turn on/off the test case
893 | requestBody jsonrpc.JSONRPCRequest
894 | requestHeader map[string]string
895 | wantStatusCode int
896 | wantBody string
897 | }{
898 | {
899 | name: "MCP Invoke my-tool",
900 | api: "http://127.0.0.1:5000/mcp",
901 | enabled: true,
902 | requestHeader: map[string]string{},
903 | requestBody: jsonrpc.JSONRPCRequest{
904 | Jsonrpc: "2.0",
905 | Id: "my-tool",
906 | Request: jsonrpc.Request{
907 | Method: "tools/call",
908 | },
909 | Params: map[string]any{
910 | "name": "my-tool",
911 | "arguments": map[string]any{
912 | "id": int(3),
913 | "name": "Alice",
914 | },
915 | },
916 | },
917 | wantStatusCode: http.StatusOK,
918 | wantBody: configs.myToolId3NameAliceWant,
919 | },
920 | {
921 | name: "MCP Invoke invalid tool",
922 | api: "http://127.0.0.1:5000/mcp",
923 | enabled: true,
924 | requestHeader: map[string]string{},
925 | requestBody: jsonrpc.JSONRPCRequest{
926 | Jsonrpc: "2.0",
927 | Id: "invalid-tool",
928 | Request: jsonrpc.Request{
929 | Method: "tools/call",
930 | },
931 | Params: map[string]any{
932 | "name": "foo",
933 | "arguments": map[string]any{},
934 | },
935 | },
936 | wantStatusCode: http.StatusOK,
937 | wantBody: `{"jsonrpc":"2.0","id":"invalid-tool","error":{"code":-32602,"message":"invalid tool name: tool with name \"foo\" does not exist"}}`,
938 | },
939 | {
940 | name: "MCP Invoke my-tool without parameters",
941 | api: "http://127.0.0.1:5000/mcp",
942 | enabled: true,
943 | requestHeader: map[string]string{},
944 | requestBody: jsonrpc.JSONRPCRequest{
945 | Jsonrpc: "2.0",
946 | Id: "invoke-without-parameter",
947 | Request: jsonrpc.Request{
948 | Method: "tools/call",
949 | },
950 | Params: map[string]any{
951 | "name": "my-tool",
952 | "arguments": map[string]any{},
953 | },
954 | },
955 | wantStatusCode: http.StatusOK,
956 | wantBody: `{"jsonrpc":"2.0","id":"invoke-without-parameter","error":{"code":-32602,"message":"provided parameters were invalid: parameter \"id\" is required"}}`,
957 | },
958 | {
959 | name: "MCP Invoke my-tool with insufficient parameters",
960 | api: "http://127.0.0.1:5000/mcp",
961 | enabled: true,
962 | requestHeader: map[string]string{},
963 | requestBody: jsonrpc.JSONRPCRequest{
964 | Jsonrpc: "2.0",
965 | Id: "invoke-insufficient-parameter",
966 | Request: jsonrpc.Request{
967 | Method: "tools/call",
968 | },
969 | Params: map[string]any{
970 | "name": "my-tool",
971 | "arguments": map[string]any{"id": 1},
972 | },
973 | },
974 | wantStatusCode: http.StatusOK,
975 | wantBody: `{"jsonrpc":"2.0","id":"invoke-insufficient-parameter","error":{"code":-32602,"message":"provided parameters were invalid: parameter \"name\" is required"}}`,
976 | },
977 | {
978 | name: "MCP Invoke my-auth-required-tool",
979 | api: "http://127.0.0.1:5000/mcp",
980 | enabled: configs.supportSelect1Auth,
981 | requestHeader: map[string]string{"my-google-auth_token": idToken},
982 | requestBody: jsonrpc.JSONRPCRequest{
983 | Jsonrpc: "2.0",
984 | Id: "invoke my-auth-required-tool",
985 | Request: jsonrpc.Request{
986 | Method: "tools/call",
987 | },
988 | Params: map[string]any{
989 | "name": "my-auth-required-tool",
990 | "arguments": map[string]any{},
991 | },
992 | },
993 | wantStatusCode: http.StatusOK,
994 | wantBody: select1Want,
995 | },
996 | {
997 | name: "MCP Invoke my-auth-required-tool with invalid auth token",
998 | api: "http://127.0.0.1:5000/mcp",
999 | requestHeader: map[string]string{"my-google-auth_token": "INVALID_TOKEN"},
1000 | requestBody: jsonrpc.JSONRPCRequest{
1001 | Jsonrpc: "2.0",
1002 | Id: "invoke my-auth-required-tool with invalid token",
1003 | Request: jsonrpc.Request{
1004 | Method: "tools/call",
1005 | },
1006 | Params: map[string]any{
1007 | "name": "my-auth-required-tool",
1008 | "arguments": map[string]any{},
1009 | },
1010 | },
1011 | wantStatusCode: http.StatusUnauthorized,
1012 | wantBody: "{\"jsonrpc\":\"2.0\",\"id\":\"invoke my-auth-required-tool with invalid token\",\"error\":{\"code\":-32600,\"message\":\"unauthorized Tool call: Please make sure your specify correct auth headers: unauthorized\"}}",
1013 | },
1014 | {
1015 | name: "MCP Invoke my-auth-required-tool without auth token",
1016 | api: "http://127.0.0.1:5000/mcp",
1017 | requestHeader: map[string]string{},
1018 | requestBody: jsonrpc.JSONRPCRequest{
1019 | Jsonrpc: "2.0",
1020 | Id: "invoke my-auth-required-tool without token",
1021 | Request: jsonrpc.Request{
1022 | Method: "tools/call",
1023 | },
1024 | Params: map[string]any{
1025 | "name": "my-auth-required-tool",
1026 | "arguments": map[string]any{},
1027 | },
1028 | },
1029 | wantStatusCode: http.StatusUnauthorized,
1030 | wantBody: "{\"jsonrpc\":\"2.0\",\"id\":\"invoke my-auth-required-tool without token\",\"error\":{\"code\":-32600,\"message\":\"unauthorized Tool call: Please make sure your specify correct auth headers: unauthorized\"}}",
1031 | },
1032 |
1033 | {
1034 | name: "MCP Invoke my-client-auth-tool",
1035 | enabled: configs.supportClientAuth,
1036 | api: "http://127.0.0.1:5000/mcp",
1037 | requestHeader: map[string]string{"Authorization": accessToken},
1038 | requestBody: jsonrpc.JSONRPCRequest{
1039 | Jsonrpc: "2.0",
1040 | Id: "invoke my-client-auth-tool",
1041 | Request: jsonrpc.Request{
1042 | Method: "tools/call",
1043 | },
1044 | Params: map[string]any{
1045 | "name": "my-client-auth-tool",
1046 | "arguments": map[string]any{},
1047 | },
1048 | },
1049 | wantStatusCode: http.StatusOK,
1050 | wantBody: "{\"jsonrpc\":\"2.0\",\"id\":\"invoke my-client-auth-tool\",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"{\\\"f0_\\\":1}\"}]}}",
1051 | },
1052 | {
1053 | name: "MCP Invoke my-client-auth-tool without access token",
1054 | enabled: configs.supportClientAuth,
1055 | api: "http://127.0.0.1:5000/mcp",
1056 | requestHeader: map[string]string{},
1057 | requestBody: jsonrpc.JSONRPCRequest{
1058 | Jsonrpc: "2.0",
1059 | Id: "invoke my-client-auth-tool",
1060 | Request: jsonrpc.Request{
1061 | Method: "tools/call",
1062 | },
1063 | Params: map[string]any{
1064 | "name": "my-client-auth-tool",
1065 | "arguments": map[string]any{},
1066 | },
1067 | },
1068 | wantStatusCode: http.StatusUnauthorized,
1069 | wantBody: "{\"jsonrpc\":\"2.0\",\"id\":\"invoke my-client-auth-tool\",\"error\":{\"code\":-32600,\"message\":\"missing access token in the 'Authorization' header\"}",
1070 | },
1071 | {
1072 | name: "MCP Invoke my-client-auth-tool with invalid access token",
1073 | enabled: configs.supportClientAuth,
1074 | api: "http://127.0.0.1:5000/mcp",
1075 | requestHeader: map[string]string{"Authorization": "Bearer invalid-token"},
1076 | requestBody: jsonrpc.JSONRPCRequest{
1077 | Jsonrpc: "2.0",
1078 | Id: "invoke my-client-auth-tool",
1079 | Request: jsonrpc.Request{
1080 | Method: "tools/call",
1081 | },
1082 | Params: map[string]any{
1083 | "name": "my-client-auth-tool",
1084 | "arguments": map[string]any{},
1085 | },
1086 | },
1087 | wantStatusCode: http.StatusUnauthorized,
1088 | },
1089 | {
1090 | name: "MCP Invoke my-fail-tool",
1091 | api: "http://127.0.0.1:5000/mcp",
1092 | enabled: true,
1093 | requestHeader: map[string]string{},
1094 | requestBody: jsonrpc.JSONRPCRequest{
1095 | Jsonrpc: "2.0",
1096 | Id: "invoke-fail-tool",
1097 | Request: jsonrpc.Request{
1098 | Method: "tools/call",
1099 | },
1100 | Params: map[string]any{
1101 | "name": "my-fail-tool",
1102 | "arguments": map[string]any{"id": 1},
1103 | },
1104 | },
1105 | wantStatusCode: http.StatusOK,
1106 | wantBody: myFailToolWant,
1107 | },
1108 | }
1109 | for _, tc := range invokeTcs {
1110 | t.Run(tc.name, func(t *testing.T) {
1111 | if !tc.enabled {
1112 | return
1113 | }
1114 | reqMarshal, err := json.Marshal(tc.requestBody)
1115 | if err != nil {
1116 | t.Fatalf("unexpected error during marshaling of request body")
1117 | }
1118 |
1119 | // add headers
1120 | headers := map[string]string{}
1121 | if sessionId != "" {
1122 | headers["Mcp-Session-Id"] = sessionId
1123 | }
1124 | for key, value := range tc.requestHeader {
1125 | headers[key] = value
1126 | }
1127 |
1128 | httpResponse, respBody := RunRequest(t, http.MethodPost, tc.api, bytes.NewBuffer(reqMarshal), headers)
1129 |
1130 | // Check status code
1131 | if httpResponse.StatusCode != tc.wantStatusCode {
1132 | t.Errorf("StatusCode mismatch: got %d, want %d", httpResponse.StatusCode, tc.wantStatusCode)
1133 | }
1134 |
1135 | // Check response body
1136 | got := string(bytes.TrimSpace(respBody))
1137 | if !strings.Contains(got, tc.wantBody) {
1138 | t.Fatalf("Expected substring not found:\ngot: %q\nwant: %q (to be contained within got)", got, tc.wantBody)
1139 | }
1140 | })
1141 | }
1142 | }
1143 |
1144 | func setupPostgresSchemas(t *testing.T, ctx context.Context, pool *pgxpool.Pool, schemaName string) func() {
1145 | createSchemaStmt := fmt.Sprintf("CREATE SCHEMA %s", schemaName)
1146 | _, err := pool.Exec(ctx, createSchemaStmt)
1147 | if err != nil {
1148 | t.Fatalf("failed to create schema: %v", err)
1149 | }
1150 |
1151 | return func() {
1152 | dropSchemaStmt := fmt.Sprintf("DROP SCHEMA %s", schemaName)
1153 | _, err := pool.Exec(ctx, dropSchemaStmt)
1154 | if err != nil {
1155 | t.Fatalf("failed to drop schema: %v", err)
1156 | }
1157 | }
1158 | }
1159 |
1160 | func RunPostgresListSchemasTest(t *testing.T, ctx context.Context, pool *pgxpool.Pool) {
1161 | schemaName := "test_schema_" + strings.ReplaceAll(uuid.New().String(), "-", "")
1162 | cleanup := setupPostgresSchemas(t, ctx, pool, schemaName)
1163 | defer cleanup()
1164 |
1165 | wantSchema := map[string]any{"functions": float64(0), "grants": map[string]any{}, "owner": "postgres", "schema_name": schemaName, "tables": float64(0), "views": float64(0)}
1166 |
1167 | invokeTcs := []struct {
1168 | name string
1169 | requestBody io.Reader
1170 | wantStatusCode int
1171 | want []map[string]any
1172 | }{
1173 | {
1174 | name: "invoke list_schemas with schema_name",
1175 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"schema_name": "%s"}`, schemaName))),
1176 | wantStatusCode: http.StatusOK,
1177 | want: []map[string]any{wantSchema},
1178 | },
1179 | {
1180 | name: "invoke list_schemas with non-existent schema",
1181 | requestBody: bytes.NewBuffer([]byte(`{"schema_name": "non_existent_schema"}`)),
1182 | wantStatusCode: http.StatusOK,
1183 | want: nil,
1184 | },
1185 | }
1186 | for _, tc := range invokeTcs {
1187 | t.Run(tc.name, func(t *testing.T) {
1188 | const api = "http://127.0.0.1:5000/api/tool/list_schemas/invoke"
1189 | req, err := http.NewRequest(http.MethodPost, api, tc.requestBody)
1190 | if err != nil {
1191 | t.Fatalf("unable to create request: %v", err)
1192 | }
1193 | req.Header.Add("Content-type", "application/json")
1194 | resp, err := http.DefaultClient.Do(req)
1195 | if err != nil {
1196 | t.Fatalf("unable to send request: %v", err)
1197 | }
1198 | defer resp.Body.Close()
1199 |
1200 | if resp.StatusCode != tc.wantStatusCode {
1201 | body, _ := io.ReadAll(resp.Body)
1202 | t.Fatalf("wrong status code: got %d, want %d, body: %s", resp.StatusCode, tc.wantStatusCode, string(body))
1203 | }
1204 | if tc.wantStatusCode != http.StatusOK {
1205 | return
1206 | }
1207 |
1208 | var bodyWrapper struct {
1209 | Result json.RawMessage `json:"result"`
1210 | }
1211 | if err := json.NewDecoder(resp.Body).Decode(&bodyWrapper); err != nil {
1212 | t.Fatalf("error decoding response wrapper: %v", err)
1213 | }
1214 |
1215 | var resultString string
1216 | if err := json.Unmarshal(bodyWrapper.Result, &resultString); err != nil {
1217 | resultString = string(bodyWrapper.Result)
1218 | }
1219 |
1220 | var got []map[string]any
1221 | if err := json.Unmarshal([]byte(resultString), &got); err != nil {
1222 | t.Fatalf("failed to unmarshal nested result string: %v", err)
1223 | }
1224 |
1225 | if diff := cmp.Diff(tc.want, got); diff != "" {
1226 | t.Errorf("Unexpected result (-want +got):\n%s", diff)
1227 | }
1228 | })
1229 | }
1230 | }
1231 |
1232 | // RunMySQLListTablesTest run tests against the mysql-list-tables tool
1233 | func RunMySQLListTablesTest(t *testing.T, databaseName, tableNameParam, tableNameAuth string) {
1234 | type tableInfo struct {
1235 | ObjectName string `json:"object_name"`
1236 | SchemaName string `json:"schema_name"`
1237 | ObjectDetails string `json:"object_details"`
1238 | }
1239 |
1240 | type column struct {
1241 | DataType string `json:"data_type"`
1242 | ColumnName string `json:"column_name"`
1243 | ColumnComment string `json:"column_comment"`
1244 | ColumnDefault any `json:"column_default"`
1245 | IsNotNullable int `json:"is_not_nullable"`
1246 | OrdinalPosition int `json:"ordinal_position"`
1247 | }
1248 |
1249 | type objectDetails struct {
1250 | Owner any `json:"owner"`
1251 | Columns []column `json:"columns"`
1252 | Comment string `json:"comment"`
1253 | Indexes []any `json:"indexes"`
1254 | Triggers []any `json:"triggers"`
1255 | Constraints []any `json:"constraints"`
1256 | ObjectName string `json:"object_name"`
1257 | ObjectType string `json:"object_type"`
1258 | SchemaName string `json:"schema_name"`
1259 | }
1260 |
1261 | paramTableWant := objectDetails{
1262 | ObjectName: tableNameParam,
1263 | SchemaName: databaseName,
1264 | ObjectType: "TABLE",
1265 | Columns: []column{
1266 | {DataType: "int", ColumnName: "id", IsNotNullable: 1, OrdinalPosition: 1},
1267 | {DataType: "varchar(255)", ColumnName: "name", OrdinalPosition: 2},
1268 | },
1269 | Indexes: []any{map[string]any{"index_columns": []any{"id"}, "index_name": "PRIMARY", "is_primary": float64(1), "is_unique": float64(1)}},
1270 | Triggers: []any{},
1271 | Constraints: []any{map[string]any{"constraint_columns": []any{"id"}, "constraint_name": "PRIMARY", "constraint_type": "PRIMARY KEY", "foreign_key_referenced_columns": any(nil), "foreign_key_referenced_table": any(nil), "constraint_definition": ""}},
1272 | }
1273 |
1274 | authTableWant := objectDetails{
1275 | ObjectName: tableNameAuth,
1276 | SchemaName: databaseName,
1277 | ObjectType: "TABLE",
1278 | Columns: []column{
1279 | {DataType: "int", ColumnName: "id", IsNotNullable: 1, OrdinalPosition: 1},
1280 | {DataType: "varchar(255)", ColumnName: "name", OrdinalPosition: 2},
1281 | {DataType: "varchar(255)", ColumnName: "email", OrdinalPosition: 3},
1282 | },
1283 | Indexes: []any{map[string]any{"index_columns": []any{"id"}, "index_name": "PRIMARY", "is_primary": float64(1), "is_unique": float64(1)}},
1284 | Triggers: []any{},
1285 | Constraints: []any{map[string]any{"constraint_columns": []any{"id"}, "constraint_name": "PRIMARY", "constraint_type": "PRIMARY KEY", "foreign_key_referenced_columns": any(nil), "foreign_key_referenced_table": any(nil), "constraint_definition": ""}},
1286 | }
1287 |
1288 | invokeTcs := []struct {
1289 | name string
1290 | requestBody io.Reader
1291 | wantStatusCode int
1292 | want any
1293 | isSimple bool
1294 | isAllTables bool
1295 | }{
1296 | {
1297 | name: "invoke list_tables for all tables detailed output",
1298 | requestBody: bytes.NewBufferString(`{"table_names":""}`),
1299 | wantStatusCode: http.StatusOK,
1300 | want: []objectDetails{authTableWant, paramTableWant},
1301 | isAllTables: true,
1302 | },
1303 | {
1304 | name: "invoke list_tables detailed output",
1305 | requestBody: bytes.NewBufferString(fmt.Sprintf(`{"table_names": "%s"}`, tableNameAuth)),
1306 | wantStatusCode: http.StatusOK,
1307 | want: []objectDetails{authTableWant},
1308 | },
1309 | {
1310 | name: "invoke list_tables simple output",
1311 | requestBody: bytes.NewBufferString(fmt.Sprintf(`{"table_names": "%s", "output_format": "simple"}`, tableNameAuth)),
1312 | wantStatusCode: http.StatusOK,
1313 | want: []map[string]any{{"name": tableNameAuth}},
1314 | isSimple: true,
1315 | },
1316 | {
1317 | name: "invoke list_tables with multiple table names",
1318 | requestBody: bytes.NewBufferString(fmt.Sprintf(`{"table_names": "%s,%s"}`, tableNameParam, tableNameAuth)),
1319 | wantStatusCode: http.StatusOK,
1320 | want: []objectDetails{authTableWant, paramTableWant},
1321 | },
1322 | {
1323 | name: "invoke list_tables with one existing and one non-existent table",
1324 | requestBody: bytes.NewBufferString(fmt.Sprintf(`{"table_names": "%s,non_existent_table"}`, tableNameAuth)),
1325 | wantStatusCode: http.StatusOK,
1326 | want: []objectDetails{authTableWant},
1327 | },
1328 | {
1329 | name: "invoke list_tables with non-existent table",
1330 | requestBody: bytes.NewBufferString(`{"table_names": "non_existent_table"}`),
1331 | wantStatusCode: http.StatusOK,
1332 | want: nil,
1333 | },
1334 | }
1335 | for _, tc := range invokeTcs {
1336 | t.Run(tc.name, func(t *testing.T) {
1337 | const api = "http://127.0.0.1:5000/api/tool/list_tables/invoke"
1338 | req, err := http.NewRequest(http.MethodPost, api, tc.requestBody)
1339 | if err != nil {
1340 | t.Fatalf("unable to create request: %v", err)
1341 | }
1342 | req.Header.Add("Content-type", "application/json")
1343 |
1344 | resp, err := http.DefaultClient.Do(req)
1345 | if err != nil {
1346 | t.Fatalf("unable to send request: %v", err)
1347 | }
1348 | defer resp.Body.Close()
1349 |
1350 | if resp.StatusCode != tc.wantStatusCode {
1351 | body, _ := io.ReadAll(resp.Body)
1352 | t.Fatalf("wrong status code: got %d, want %d, body: %s", resp.StatusCode, tc.wantStatusCode, string(body))
1353 | }
1354 | if tc.wantStatusCode != http.StatusOK {
1355 | return
1356 | }
1357 |
1358 | var bodyWrapper struct {
1359 | Result json.RawMessage `json:"result"`
1360 | }
1361 | if err := json.NewDecoder(resp.Body).Decode(&bodyWrapper); err != nil {
1362 | t.Fatalf("error decoding response wrapper: %v", err)
1363 | }
1364 |
1365 | var resultString string
1366 | if err := json.Unmarshal(bodyWrapper.Result, &resultString); err != nil {
1367 | resultString = string(bodyWrapper.Result)
1368 | }
1369 |
1370 | var got any
1371 | if tc.isSimple {
1372 | var tables []tableInfo
1373 | if err := json.Unmarshal([]byte(resultString), &tables); err != nil {
1374 | t.Fatalf("failed to unmarshal outer JSON array into []tableInfo: %v", err)
1375 | }
1376 | var details []map[string]any
1377 | for _, table := range tables {
1378 | var d map[string]any
1379 | if err := json.Unmarshal([]byte(table.ObjectDetails), &d); err != nil {
1380 | t.Fatalf("failed to unmarshal nested ObjectDetails string: %v", err)
1381 | }
1382 | details = append(details, d)
1383 | }
1384 | got = details
1385 | } else {
1386 | if resultString == "null" {
1387 | got = nil
1388 | } else {
1389 | var tables []tableInfo
1390 | if err := json.Unmarshal([]byte(resultString), &tables); err != nil {
1391 | t.Fatalf("failed to unmarshal outer JSON array into []tableInfo: %v", err)
1392 | }
1393 | var details []objectDetails
1394 | for _, table := range tables {
1395 | var d objectDetails
1396 | if err := json.Unmarshal([]byte(table.ObjectDetails), &d); err != nil {
1397 | t.Fatalf("failed to unmarshal nested ObjectDetails string: %v", err)
1398 | }
1399 | details = append(details, d)
1400 | }
1401 | got = details
1402 | }
1403 | }
1404 |
1405 | opts := []cmp.Option{
1406 | cmpopts.SortSlices(func(a, b objectDetails) bool { return a.ObjectName < b.ObjectName }),
1407 | cmpopts.SortSlices(func(a, b column) bool { return a.ColumnName < b.ColumnName }),
1408 | cmpopts.SortSlices(func(a, b map[string]any) bool { return a["name"].(string) < b["name"].(string) }),
1409 | }
1410 |
1411 | // Checking only the current database where the test tables are created to avoid brittle tests.
1412 | if tc.isAllTables {
1413 | var filteredGot []objectDetails
1414 | if got != nil {
1415 | for _, item := range got.([]objectDetails) {
1416 | if item.SchemaName == databaseName {
1417 | filteredGot = append(filteredGot, item)
1418 | }
1419 | }
1420 | }
1421 | if len(filteredGot) == 0 {
1422 | got = nil
1423 | } else {
1424 | got = filteredGot
1425 | }
1426 | }
1427 |
1428 | if diff := cmp.Diff(tc.want, got, opts...); diff != "" {
1429 | t.Errorf("Unexpected result: got %#v, want: %#v", got, tc.want)
1430 | }
1431 | })
1432 | }
1433 | }
1434 |
1435 | // RunMySQLListActiveQueriesTest run tests against the mysql-list-active-queries tests
1436 | func RunMySQLListActiveQueriesTest(t *testing.T, ctx context.Context, pool *sql.DB) {
1437 | type queryListDetails struct {
1438 | ProcessId any `json:"process_id"`
1439 | Query string `json:"query"`
1440 | TrxStarted any `json:"trx_started"`
1441 | TrxDuration any `json:"trx_duration_seconds"`
1442 | TrxWaitDuration any `json:"trx_wait_duration_seconds"`
1443 | QueryTime any `json:"query_time"`
1444 | TrxState string `json:"trx_state"`
1445 | ProcessState string `json:"process_state"`
1446 | User string `json:"user"`
1447 | TrxRowsLocked any `json:"trx_rows_locked"`
1448 | TrxRowsModified any `json:"trx_rows_modified"`
1449 | Db string `json:"db"`
1450 | }
1451 |
1452 | singleQueryWanted := queryListDetails{
1453 | ProcessId: any(nil),
1454 | Query: "SELECT sleep(10)",
1455 | TrxStarted: any(nil),
1456 | TrxDuration: any(nil),
1457 | TrxWaitDuration: any(nil),
1458 | QueryTime: any(nil),
1459 | TrxState: "",
1460 | ProcessState: "User sleep",
1461 | User: "",
1462 | TrxRowsLocked: any(nil),
1463 | TrxRowsModified: any(nil),
1464 | Db: "",
1465 | }
1466 |
1467 | invokeTcs := []struct {
1468 | name string
1469 | requestBody io.Reader
1470 | clientSleepSecs int
1471 | waitSecsBeforeCheck int
1472 | wantStatusCode int
1473 | want any
1474 | }{
1475 | {
1476 | name: "invoke list_active_queries when the system is idle",
1477 | requestBody: bytes.NewBufferString(`{}`),
1478 | clientSleepSecs: 0,
1479 | waitSecsBeforeCheck: 0,
1480 | wantStatusCode: http.StatusOK,
1481 | want: []queryListDetails(nil),
1482 | },
1483 | {
1484 | name: "invoke list_active_queries when there is 1 ongoing but lower than the threshold",
1485 | requestBody: bytes.NewBufferString(`{"min_duration_secs": 100}`),
1486 | clientSleepSecs: 10,
1487 | waitSecsBeforeCheck: 1,
1488 | wantStatusCode: http.StatusOK,
1489 | want: []queryListDetails(nil),
1490 | },
1491 | {
1492 | name: "invoke list_active_queries when 1 ongoing query should show up",
1493 | requestBody: bytes.NewBufferString(`{"min_duration_secs": 5}`),
1494 | clientSleepSecs: 0,
1495 | waitSecsBeforeCheck: 5,
1496 | wantStatusCode: http.StatusOK,
1497 | want: []queryListDetails{singleQueryWanted},
1498 | },
1499 | {
1500 | name: "invoke list_active_queries when 2 ongoing query should show up",
1501 | requestBody: bytes.NewBufferString(`{"min_duration_secs": 2}`),
1502 | clientSleepSecs: 10,
1503 | waitSecsBeforeCheck: 3,
1504 | wantStatusCode: http.StatusOK,
1505 | want: []queryListDetails{singleQueryWanted, singleQueryWanted},
1506 | },
1507 | }
1508 |
1509 | var wg sync.WaitGroup
1510 | for _, tc := range invokeTcs {
1511 | t.Run(tc.name, func(t *testing.T) {
1512 | if tc.clientSleepSecs > 0 {
1513 | wg.Add(1)
1514 |
1515 | go func() {
1516 | defer wg.Done()
1517 |
1518 | err := pool.PingContext(ctx)
1519 | if err != nil {
1520 | t.Errorf("unable to connect to test database: %s", err)
1521 | return
1522 | }
1523 | _, err = pool.ExecContext(ctx, fmt.Sprintf("SELECT sleep(%d);", tc.clientSleepSecs))
1524 | if err != nil {
1525 | t.Errorf("Executing 'SELECT sleep' failed: %s", err)
1526 | }
1527 | }()
1528 | }
1529 |
1530 | if tc.waitSecsBeforeCheck > 0 {
1531 | time.Sleep(time.Duration(tc.waitSecsBeforeCheck) * time.Second)
1532 | }
1533 |
1534 | const api = "http://127.0.0.1:5000/api/tool/list_active_queries/invoke"
1535 | req, err := http.NewRequest(http.MethodPost, api, tc.requestBody)
1536 | if err != nil {
1537 | t.Fatalf("unable to create request: %v", err)
1538 | }
1539 | req.Header.Add("Content-type", "application/json")
1540 |
1541 | resp, err := http.DefaultClient.Do(req)
1542 | if err != nil {
1543 | t.Fatalf("unable to send request: %v", err)
1544 | }
1545 | defer resp.Body.Close()
1546 |
1547 | if resp.StatusCode != tc.wantStatusCode {
1548 | body, _ := io.ReadAll(resp.Body)
1549 | t.Fatalf("wrong status code: got %d, want %d, body: %s", resp.StatusCode, tc.wantStatusCode, string(body))
1550 | }
1551 | if tc.wantStatusCode != http.StatusOK {
1552 | return
1553 | }
1554 |
1555 | var bodyWrapper struct {
1556 | Result json.RawMessage `json:"result"`
1557 | }
1558 | if err := json.NewDecoder(resp.Body).Decode(&bodyWrapper); err != nil {
1559 | t.Fatalf("error decoding response wrapper: %v", err)
1560 | }
1561 |
1562 | var resultString string
1563 | if err := json.Unmarshal(bodyWrapper.Result, &resultString); err != nil {
1564 | resultString = string(bodyWrapper.Result)
1565 | }
1566 |
1567 | var got any
1568 | var details []queryListDetails
1569 | if err := json.Unmarshal([]byte(resultString), &details); err != nil {
1570 | t.Fatalf("failed to unmarshal nested ObjectDetails string: %v", err)
1571 | }
1572 | got = details
1573 |
1574 | if diff := cmp.Diff(tc.want, got, cmp.Comparer(func(a, b queryListDetails) bool {
1575 | return a.Query == b.Query && a.ProcessState == b.ProcessState
1576 | })); diff != "" {
1577 | t.Errorf("Unexpected result: got %#v, want: %#v", got, tc.want)
1578 | }
1579 | })
1580 | }
1581 | wg.Wait()
1582 | }
1583 |
1584 | func RunMySQLListTablesMissingUniqueIndexes(t *testing.T, ctx context.Context, pool *sql.DB, databaseName string) {
1585 | type listDetails struct {
1586 | TableSchema string `json:"table_schema"`
1587 | TableName string `json:"table_name"`
1588 | }
1589 |
1590 | // bunch of wanted
1591 | nonUniqueKeyTableName := "t03_non_unqiue_key_table"
1592 | noKeyTableName := "t04_no_key_table"
1593 | nonUniqueKeyTableWant := listDetails{
1594 | TableSchema: databaseName,
1595 | TableName: nonUniqueKeyTableName,
1596 | }
1597 | noKeyTableWant := listDetails{
1598 | TableSchema: databaseName,
1599 | TableName: noKeyTableName,
1600 | }
1601 |
1602 | invokeTcs := []struct {
1603 | name string
1604 | requestBody io.Reader
1605 | newTableName string
1606 | newTablePrimaryKey bool
1607 | newTableUniqueKey bool
1608 | newTableNonUniqueKey bool
1609 | wantStatusCode int
1610 | want any
1611 | }{
1612 | {
1613 | name: "invoke list_tables_missing_unique_indexes when nothing to be found",
1614 | requestBody: bytes.NewBufferString(`{}`),
1615 | newTableName: "",
1616 | newTablePrimaryKey: false,
1617 | newTableUniqueKey: false,
1618 | newTableNonUniqueKey: false,
1619 | wantStatusCode: http.StatusOK,
1620 | want: []listDetails(nil),
1621 | },
1622 | {
1623 | name: "invoke list_tables_missing_unique_indexes pk table will not show",
1624 | requestBody: bytes.NewBufferString(`{}`),
1625 | newTableName: "t01",
1626 | newTablePrimaryKey: true,
1627 | newTableUniqueKey: false,
1628 | newTableNonUniqueKey: false,
1629 | wantStatusCode: http.StatusOK,
1630 | want: []listDetails(nil),
1631 | },
1632 | {
1633 | name: "invoke list_tables_missing_unique_indexes uk table will not show",
1634 | requestBody: bytes.NewBufferString(`{}`),
1635 | newTableName: "t02",
1636 | newTablePrimaryKey: false,
1637 | newTableUniqueKey: true,
1638 | newTableNonUniqueKey: false,
1639 | wantStatusCode: http.StatusOK,
1640 | want: []listDetails(nil),
1641 | },
1642 | {
1643 | name: "invoke list_tables_missing_unique_indexes non-unique key only table will show",
1644 | requestBody: bytes.NewBufferString(`{}`),
1645 | newTableName: nonUniqueKeyTableName,
1646 | newTablePrimaryKey: false,
1647 | newTableUniqueKey: false,
1648 | newTableNonUniqueKey: true,
1649 | wantStatusCode: http.StatusOK,
1650 | want: []listDetails{nonUniqueKeyTableWant},
1651 | },
1652 | {
1653 | name: "invoke list_tables_missing_unique_indexes table with no key at all will show",
1654 | requestBody: bytes.NewBufferString(`{}`),
1655 | newTableName: noKeyTableName,
1656 | newTablePrimaryKey: false,
1657 | newTableUniqueKey: false,
1658 | newTableNonUniqueKey: false,
1659 | wantStatusCode: http.StatusOK,
1660 | want: []listDetails{nonUniqueKeyTableWant, noKeyTableWant},
1661 | },
1662 | {
1663 | name: "invoke list_tables_missing_unique_indexes table w/ both pk & uk will not show",
1664 | requestBody: bytes.NewBufferString(`{}`),
1665 | newTableName: "t05",
1666 | newTablePrimaryKey: true,
1667 | newTableUniqueKey: true,
1668 | newTableNonUniqueKey: false,
1669 | wantStatusCode: http.StatusOK,
1670 | want: []listDetails{nonUniqueKeyTableWant, noKeyTableWant},
1671 | },
1672 | {
1673 | name: "invoke list_tables_missing_unique_indexes table w/ uk & nk will not show",
1674 | requestBody: bytes.NewBufferString(`{}`),
1675 | newTableName: "t06",
1676 | newTablePrimaryKey: false,
1677 | newTableUniqueKey: true,
1678 | newTableNonUniqueKey: true,
1679 | wantStatusCode: http.StatusOK,
1680 | want: []listDetails{nonUniqueKeyTableWant, noKeyTableWant},
1681 | },
1682 | {
1683 | name: "invoke list_tables_missing_unique_indexes table w/ pk & nk will not show",
1684 | requestBody: bytes.NewBufferString(`{}`),
1685 | newTableName: "t07",
1686 | newTablePrimaryKey: true,
1687 | newTableUniqueKey: false,
1688 | newTableNonUniqueKey: true,
1689 | wantStatusCode: http.StatusOK,
1690 | want: []listDetails{nonUniqueKeyTableWant, noKeyTableWant},
1691 | },
1692 | {
1693 | name: "invoke list_tables_missing_unique_indexes with a non-exist database, nothing to show",
1694 | requestBody: bytes.NewBufferString(`{"table_schema": "non-exist-database"}`),
1695 | newTableName: "",
1696 | newTablePrimaryKey: false,
1697 | newTableUniqueKey: false,
1698 | newTableNonUniqueKey: false,
1699 | wantStatusCode: http.StatusOK,
1700 | want: []listDetails(nil),
1701 | },
1702 | {
1703 | name: "invoke list_tables_missing_unique_indexes with the right database, show everything",
1704 | requestBody: bytes.NewBufferString(fmt.Sprintf(`{"table_schema": "%s"}`, databaseName)),
1705 | newTableName: "",
1706 | newTablePrimaryKey: false,
1707 | newTableUniqueKey: false,
1708 | newTableNonUniqueKey: false,
1709 | wantStatusCode: http.StatusOK,
1710 | want: []listDetails{nonUniqueKeyTableWant, noKeyTableWant},
1711 | },
1712 | {
1713 | name: "invoke list_tables_missing_unique_indexes with limited output",
1714 | requestBody: bytes.NewBufferString(`{"limit": 1}`),
1715 | newTableName: "",
1716 | newTablePrimaryKey: false,
1717 | newTableUniqueKey: false,
1718 | newTableNonUniqueKey: false,
1719 | wantStatusCode: http.StatusOK,
1720 | want: []listDetails{nonUniqueKeyTableWant},
1721 | },
1722 | }
1723 |
1724 | createTableHelper := func(t *testing.T, tableName, databaseName string, primaryKey, uniqueKey, nonUniqueKey bool, ctx context.Context, pool *sql.DB) func() {
1725 | var stmt strings.Builder
1726 | stmt.WriteString(fmt.Sprintf("CREATE TABLE %s (", tableName))
1727 | stmt.WriteString("c1 INT")
1728 | if primaryKey {
1729 | stmt.WriteString(" PRIMARY KEY")
1730 | }
1731 | stmt.WriteString(", c2 INT, c3 CHAR(8)")
1732 | if uniqueKey {
1733 | stmt.WriteString(", UNIQUE(c2)")
1734 | }
1735 | if nonUniqueKey {
1736 | stmt.WriteString(", INDEX(c3)")
1737 | }
1738 | stmt.WriteString(")")
1739 |
1740 | t.Logf("Creating table: %s", stmt.String())
1741 | if _, err := pool.ExecContext(ctx, stmt.String()); err != nil {
1742 | t.Fatalf("failed executing %s: %v", stmt.String(), err)
1743 | }
1744 |
1745 | return func() {
1746 | t.Logf("Dropping table: %s", tableName)
1747 | if _, err := pool.ExecContext(ctx, fmt.Sprintf("DROP TABLE %s", tableName)); err != nil {
1748 | t.Errorf("failed to drop table %s: %v", tableName, err)
1749 | }
1750 | }
1751 | }
1752 |
1753 | var cleanups []func()
1754 | defer func() {
1755 | for i := len(cleanups) - 1; i >= 0; i-- {
1756 | cleanups[i]()
1757 | }
1758 | }()
1759 |
1760 | for _, tc := range invokeTcs {
1761 | t.Run(tc.name, func(t *testing.T) {
1762 | if tc.newTableName != "" {
1763 | cleanup := createTableHelper(t, tc.newTableName, databaseName, tc.newTablePrimaryKey, tc.newTableUniqueKey, tc.newTableNonUniqueKey, ctx, pool)
1764 | cleanups = append(cleanups, cleanup)
1765 | }
1766 |
1767 | const api = "http://127.0.0.1:5000/api/tool/list_tables_missing_unique_indexes/invoke"
1768 | req, err := http.NewRequest(http.MethodPost, api, tc.requestBody)
1769 | if err != nil {
1770 | t.Fatalf("unable to create request: %v", err)
1771 | }
1772 | req.Header.Add("Content-type", "application/json")
1773 |
1774 | resp, err := http.DefaultClient.Do(req)
1775 | if err != nil {
1776 | t.Fatalf("unable to send request: %v", err)
1777 | }
1778 | defer resp.Body.Close()
1779 |
1780 | if resp.StatusCode != tc.wantStatusCode {
1781 | body, _ := io.ReadAll(resp.Body)
1782 | t.Fatalf("wrong status code: got %d, want %d, body: %s", resp.StatusCode, tc.wantStatusCode, string(body))
1783 | }
1784 | if tc.wantStatusCode != http.StatusOK {
1785 | return
1786 | }
1787 |
1788 | var bodyWrapper struct {
1789 | Result json.RawMessage `json:"result"`
1790 | }
1791 | if err := json.NewDecoder(resp.Body).Decode(&bodyWrapper); err != nil {
1792 | t.Fatalf("error decoding response wrapper: %v", err)
1793 | }
1794 |
1795 | var resultString string
1796 | if err := json.Unmarshal(bodyWrapper.Result, &resultString); err != nil {
1797 | resultString = string(bodyWrapper.Result)
1798 | }
1799 |
1800 | var got any
1801 | var details []listDetails
1802 | if err := json.Unmarshal([]byte(resultString), &details); err != nil {
1803 | t.Fatalf("failed to unmarshal nested listDetails string: %v", err)
1804 | }
1805 | got = details
1806 |
1807 | if diff := cmp.Diff(tc.want, got, cmp.Comparer(func(a, b listDetails) bool {
1808 | return a.TableSchema == b.TableSchema && a.TableName == b.TableName
1809 | })); diff != "" {
1810 | t.Errorf("Unexpected result: got %#v, want: %#v", got, tc.want)
1811 | }
1812 | })
1813 | }
1814 | }
1815 |
1816 | func RunMySQLListTableFragmentationTest(t *testing.T, databaseName, tableNameParam, tableNameAuth string) {
1817 | type tableFragmentationDetails struct {
1818 | TableSchema string `json:"table_schema"`
1819 | TableName string `json:"table_name"`
1820 | DataSize any `json:"data_size"`
1821 | IndexSize any `json:"index_size"`
1822 | DataFree any `json:"data_free"`
1823 | FragmentationPercentage any `json:"fragmentation_percentage"`
1824 | }
1825 |
1826 | paramTableEntryWanted := tableFragmentationDetails{
1827 | TableSchema: databaseName,
1828 | TableName: tableNameParam,
1829 | DataSize: any(nil),
1830 | IndexSize: any(nil),
1831 | DataFree: any(nil),
1832 | FragmentationPercentage: any(nil),
1833 | }
1834 | authTableEntryWanted := tableFragmentationDetails{
1835 | TableSchema: databaseName,
1836 | TableName: tableNameAuth,
1837 | DataSize: any(nil),
1838 | IndexSize: any(nil),
1839 | DataFree: any(nil),
1840 | FragmentationPercentage: any(nil),
1841 | }
1842 |
1843 | invokeTcs := []struct {
1844 | name string
1845 | requestBody io.Reader
1846 | wantStatusCode int
1847 | want any
1848 | }{
1849 | {
1850 | name: "invoke list_table_fragmentation on all, no data_free threshold, expected to have 2 results",
1851 | requestBody: bytes.NewBufferString(`{"data_free_threshold_bytes": 0}`),
1852 | wantStatusCode: http.StatusOK,
1853 | want: []tableFragmentationDetails{authTableEntryWanted, paramTableEntryWanted},
1854 | },
1855 | {
1856 | name: "invoke list_table_fragmentation on all, no data_free threshold, limit to 1, expected to have 1 results",
1857 | requestBody: bytes.NewBufferString(`{"data_free_threshold_bytes": 0, "limit": 1}`),
1858 | wantStatusCode: http.StatusOK,
1859 | want: []tableFragmentationDetails{authTableEntryWanted},
1860 | },
1861 | {
1862 | name: "invoke list_table_fragmentation on all databases and 1 specific table name, no data_free threshold, expected to have 1 result",
1863 | requestBody: bytes.NewBufferString(fmt.Sprintf(`{"table_name": "%s","data_free_threshold_bytes": 0}`, tableNameAuth)),
1864 | wantStatusCode: http.StatusOK,
1865 | want: []tableFragmentationDetails{authTableEntryWanted},
1866 | },
1867 | {
1868 | name: "invoke list_table_fragmentation on 1 database and 1 specific table name, no data_free threshold, expected to have 1 result",
1869 | requestBody: bytes.NewBufferString(fmt.Sprintf(`{"table_schema": "%s", "table_name": "%s", "data_free_threshold_bytes": 0}`, databaseName, tableNameParam)),
1870 | wantStatusCode: http.StatusOK,
1871 | want: []tableFragmentationDetails{paramTableEntryWanted},
1872 | },
1873 | {
1874 | name: "invoke list_table_fragmentation on 1 database and 1 specific table name, high data_free threshold, expected to have 0 result",
1875 | requestBody: bytes.NewBufferString(fmt.Sprintf(`{"table_schema": "%s", "table_name": "%s", "data_free_threshold_bytes": 1000000000}`, databaseName, tableNameParam)),
1876 | wantStatusCode: http.StatusOK,
1877 | want: []tableFragmentationDetails(nil),
1878 | },
1879 | {
1880 | name: "invoke list_table_fragmentation on 1 non-exist database, no data_free threshold, expected to have 0 result",
1881 | requestBody: bytes.NewBufferString(`{"table_schema": "non_existent_database", "data_free_threshold_bytes": 0}`),
1882 | wantStatusCode: http.StatusOK,
1883 | want: []tableFragmentationDetails(nil),
1884 | },
1885 | {
1886 | name: "invoke list_table_fragmentation on 1 non-exist table, no data_free threshold, expected to have 0 result",
1887 | requestBody: bytes.NewBufferString(`{"table_name": "non_existent_table", "data_free_threshold_bytes": 0}`),
1888 | wantStatusCode: http.StatusOK,
1889 | want: []tableFragmentationDetails(nil),
1890 | },
1891 | }
1892 | for _, tc := range invokeTcs {
1893 | t.Run(tc.name, func(t *testing.T) {
1894 | const api = "http://127.0.0.1:5000/api/tool/list_table_fragmentation/invoke"
1895 | req, err := http.NewRequest(http.MethodPost, api, tc.requestBody)
1896 | if err != nil {
1897 | t.Fatalf("unable to create request: %v", err)
1898 | }
1899 | req.Header.Add("Content-type", "application/json")
1900 |
1901 | resp, err := http.DefaultClient.Do(req)
1902 | if err != nil {
1903 | t.Fatalf("unable to send request: %v", err)
1904 | }
1905 | defer resp.Body.Close()
1906 |
1907 | if resp.StatusCode != tc.wantStatusCode {
1908 | body, _ := io.ReadAll(resp.Body)
1909 | t.Fatalf("wrong status code: got %d, want %d, body: %s", resp.StatusCode, tc.wantStatusCode, string(body))
1910 | }
1911 | if tc.wantStatusCode != http.StatusOK {
1912 | return
1913 | }
1914 |
1915 | var bodyWrapper struct {
1916 | Result json.RawMessage `json:"result"`
1917 | }
1918 | if err := json.NewDecoder(resp.Body).Decode(&bodyWrapper); err != nil {
1919 | t.Fatalf("error decoding response wrapper: %v", err)
1920 | }
1921 |
1922 | var resultString string
1923 | if err := json.Unmarshal(bodyWrapper.Result, &resultString); err != nil {
1924 | resultString = string(bodyWrapper.Result)
1925 | }
1926 |
1927 | var got any
1928 | var details []tableFragmentationDetails
1929 | if err := json.Unmarshal([]byte(resultString), &details); err != nil {
1930 | t.Fatalf("failed to unmarshal outer JSON array into []tableInfo: %v", err)
1931 | }
1932 | got = details
1933 |
1934 | if diff := cmp.Diff(tc.want, got, cmp.Comparer(func(a, b tableFragmentationDetails) bool {
1935 | return a.TableSchema == b.TableSchema && a.TableName == b.TableName
1936 | })); diff != "" {
1937 | t.Errorf("Unexpected result: got %#v, want: %#v", got, tc.want)
1938 | }
1939 | })
1940 | }
1941 | }
1942 |
1943 | // RunMSSQLListTablesTest run tests againsts the mssql-list-tables tools.
1944 | func RunMSSQLListTablesTest(t *testing.T, tableNameParam, tableNameAuth string) {
1945 | // TableNameParam columns to construct want.
1946 | const paramTableColumns = `[
1947 | {"column_name": "id", "data_type": "INT", "column_ordinal_position": 1, "is_not_nullable": true},
1948 | {"column_name": "name", "data_type": "VARCHAR(255)", "column_ordinal_position": 2, "is_not_nullable": false}
1949 | ]`
1950 |
1951 | // TableNameAuth columns to construct want
1952 | const authTableColumns = `[
1953 | {"column_name": "id", "data_type": "INT", "column_ordinal_position": 1, "is_not_nullable": true},
1954 | {"column_name": "name", "data_type": "VARCHAR(255)", "column_ordinal_position": 2, "is_not_nullable": false},
1955 | {"column_name": "email", "data_type": "VARCHAR(255)", "column_ordinal_position": 3, "is_not_nullable": false}
1956 | ]`
1957 |
1958 | const (
1959 | // Template to construct detailed output want.
1960 | detailedObjectTemplate = `{
1961 | "schema_name": "dbo",
1962 | "object_name": "%[1]s",
1963 | "object_details": {
1964 | "owner": "dbo",
1965 | "triggers": [],
1966 | "columns": %[2]s,
1967 | "object_name": "%[1]s",
1968 | "object_type": "TABLE",
1969 | "schema_name": "dbo"
1970 | }
1971 | }`
1972 |
1973 | // Template to construct simple output want
1974 | simpleObjectTemplate = `{"object_name":"%s", "schema_name":"dbo", "object_details":{"name":"%s"}}`
1975 | )
1976 |
1977 | // Helper to build json for detailed want
1978 | getDetailedWant := func(tableName, columnJSON string) string {
1979 | return fmt.Sprintf(detailedObjectTemplate, tableName, columnJSON)
1980 | }
1981 |
1982 | // Helper to build template for simple want
1983 | getSimpleWant := func(tableName string) string {
1984 | return fmt.Sprintf(simpleObjectTemplate, tableName, tableName)
1985 | }
1986 |
1987 | invokeTcs := []struct {
1988 | name string
1989 | api string
1990 | requestBody string
1991 | wantStatusCode int
1992 | want string
1993 | isAllTables bool
1994 | }{
1995 | {
1996 | name: "invoke list_tables for all tables detailed output",
1997 | api: "http://127.0.0.1:5000/api/tool/list_tables/invoke",
1998 | requestBody: `{"table_names": ""}`,
1999 | wantStatusCode: http.StatusOK,
2000 | want: fmt.Sprintf("[%s,%s]", getDetailedWant(tableNameAuth, authTableColumns), getDetailedWant(tableNameParam, paramTableColumns)),
2001 | isAllTables: true,
2002 | },
2003 | {
2004 | name: "invoke list_tables for all tables simple output",
2005 | api: "http://127.0.0.1:5000/api/tool/list_tables/invoke",
2006 | requestBody: `{"table_names": "", "output_format": "simple"}`,
2007 | wantStatusCode: http.StatusOK,
2008 | want: fmt.Sprintf("[%s,%s]", getSimpleWant(tableNameAuth), getSimpleWant(tableNameParam)),
2009 | isAllTables: true,
2010 | },
2011 | {
2012 | name: "invoke list_tables detailed output",
2013 | api: "http://127.0.0.1:5000/api/tool/list_tables/invoke",
2014 | requestBody: fmt.Sprintf(`{"table_names": "%s"}`, tableNameAuth),
2015 | wantStatusCode: http.StatusOK,
2016 | want: fmt.Sprintf("[%s]", getDetailedWant(tableNameAuth, authTableColumns)),
2017 | },
2018 | {
2019 | name: "invoke list_tables simple output",
2020 | api: "http://127.0.0.1:5000/api/tool/list_tables/invoke",
2021 | requestBody: fmt.Sprintf(`{"table_names": "%s", "output_format": "simple"}`, tableNameAuth),
2022 | wantStatusCode: http.StatusOK,
2023 | want: fmt.Sprintf("[%s]", getSimpleWant(tableNameAuth)),
2024 | },
2025 | {
2026 | name: "invoke list_tables with invalid output format",
2027 | api: "http://127.0.0.1:5000/api/tool/list_tables/invoke",
2028 | requestBody: `{"table_names": "", "output_format": "abcd"}`,
2029 | wantStatusCode: http.StatusBadRequest,
2030 | },
2031 | {
2032 | name: "invoke list_tables with malformed table_names parameter",
2033 | api: "http://127.0.0.1:5000/api/tool/list_tables/invoke",
2034 | requestBody: `{"table_names": 12345, "output_format": "detailed"}`,
2035 | wantStatusCode: http.StatusBadRequest,
2036 | },
2037 | {
2038 | name: "invoke list_tables with multiple table names",
2039 | api: "http://127.0.0.1:5000/api/tool/list_tables/invoke",
2040 | requestBody: fmt.Sprintf(`{"table_names": "%s,%s"}`, tableNameParam, tableNameAuth),
2041 | wantStatusCode: http.StatusOK,
2042 | want: fmt.Sprintf("[%s,%s]", getDetailedWant(tableNameAuth, authTableColumns), getDetailedWant(tableNameParam, paramTableColumns)),
2043 | },
2044 | {
2045 | name: "invoke list_tables with non-existent table",
2046 | api: "http://127.0.0.1:5000/api/tool/list_tables/invoke",
2047 | requestBody: `{"table_names": "non_existent_table"}`,
2048 | wantStatusCode: http.StatusOK,
2049 | want: `null`,
2050 | },
2051 | {
2052 | name: "invoke list_tables with one existing and one non-existent table",
2053 | api: "http://127.0.0.1:5000/api/tool/list_tables/invoke",
2054 | requestBody: fmt.Sprintf(`{"table_names": "%s,non_existent_table"}`, tableNameParam),
2055 | wantStatusCode: http.StatusOK,
2056 | want: fmt.Sprintf("[%s]", getDetailedWant(tableNameParam, paramTableColumns)),
2057 | },
2058 | }
2059 | for _, tc := range invokeTcs {
2060 | t.Run(tc.name, func(t *testing.T) {
2061 | resp, respBytes := RunRequest(t, http.MethodPost, tc.api, bytes.NewBuffer([]byte(tc.requestBody)), nil)
2062 |
2063 | if resp.StatusCode != tc.wantStatusCode {
2064 | t.Fatalf("response status code is not %d, got %d: %s", tc.wantStatusCode, resp.StatusCode, string(respBytes))
2065 | }
2066 |
2067 | if tc.wantStatusCode == http.StatusOK {
2068 | var bodyWrapper map[string]json.RawMessage
2069 |
2070 | if err := json.Unmarshal(respBytes, &bodyWrapper); err != nil {
2071 | t.Fatalf("error parsing response wrapper: %s, body: %s", err, string(respBytes))
2072 | }
2073 |
2074 | resultJSON, ok := bodyWrapper["result"]
2075 | if !ok {
2076 | t.Fatal("unable to find 'result' in response body")
2077 | }
2078 |
2079 | var resultString string
2080 | if err := json.Unmarshal(resultJSON, &resultString); err != nil {
2081 | if string(resultJSON) == "null" {
2082 | resultString = "null"
2083 | } else {
2084 | t.Fatalf("'result' is not a JSON-encoded string: %s", err)
2085 | }
2086 | }
2087 |
2088 | var got, want []any
2089 |
2090 | if err := json.Unmarshal([]byte(resultString), &got); err != nil {
2091 | t.Fatalf("failed to unmarshal actual result string: %v", err)
2092 | }
2093 | if err := json.Unmarshal([]byte(tc.want), &want); err != nil {
2094 | t.Fatalf("failed to unmarshal expected want string: %v", err)
2095 | }
2096 |
2097 | for _, item := range got {
2098 | itemMap, ok := item.(map[string]any)
2099 | if !ok {
2100 | continue
2101 | }
2102 |
2103 | detailsStr, ok := itemMap["object_details"].(string)
2104 | if !ok {
2105 | continue
2106 | }
2107 |
2108 | var detailsMap map[string]any
2109 | if err := json.Unmarshal([]byte(detailsStr), &detailsMap); err != nil {
2110 | t.Fatalf("failed to unmarshal nested object_details string: %v", err)
2111 | }
2112 |
2113 | // clean unpredictable fields
2114 | delete(detailsMap, "constraints")
2115 | delete(detailsMap, "indexes")
2116 |
2117 | itemMap["object_details"] = detailsMap
2118 | }
2119 |
2120 | // Checking only the default dbo schema where the test tables are created to avoid brittle tests.
2121 | if tc.isAllTables {
2122 | var filteredGot []any
2123 | for _, item := range got {
2124 | if tableMap, ok := item.(map[string]interface{}); ok {
2125 | if schema, ok := tableMap["schema_name"]; ok && schema == "dbo" {
2126 | filteredGot = append(filteredGot, item)
2127 | }
2128 | }
2129 | }
2130 | got = filteredGot
2131 | }
2132 |
2133 | sort.SliceStable(got, func(i, j int) bool {
2134 | return fmt.Sprintf("%v", got[i]) < fmt.Sprintf("%v", got[j])
2135 | })
2136 | sort.SliceStable(want, func(i, j int) bool {
2137 | return fmt.Sprintf("%v", want[i]) < fmt.Sprintf("%v", want[j])
2138 | })
2139 |
2140 | if !reflect.DeepEqual(got, want) {
2141 | gotJSON, _ := json.MarshalIndent(got, "", " ")
2142 | wantJSON, _ := json.MarshalIndent(want, "", " ")
2143 | t.Errorf("Unexpected result:\ngot:\n%s\n\nwant:\n%s", string(gotJSON), string(wantJSON))
2144 | }
2145 | }
2146 | })
2147 | }
2148 | }
2149 |
2150 | // RunRequest is a helper function to send HTTP requests and return the response
2151 | func RunRequest(t *testing.T, method, url string, body io.Reader, headers map[string]string) (*http.Response, []byte) {
2152 | // Send request
2153 | req, err := http.NewRequest(method, url, body)
2154 | if err != nil {
2155 | t.Fatalf("unable to create request: %s", err)
2156 | }
2157 |
2158 | req.Header.Set("Content-type", "application/json")
2159 |
2160 | for k, v := range headers {
2161 | req.Header.Set(k, v)
2162 | }
2163 |
2164 | resp, err := http.DefaultClient.Do(req)
2165 | if err != nil {
2166 | t.Fatalf("unable to send request: %s", err)
2167 | }
2168 | respBody, err := io.ReadAll(resp.Body)
2169 | if err != nil {
2170 | t.Fatalf("unable to read request body: %s", err)
2171 | }
2172 |
2173 | defer resp.Body.Close()
2174 | return resp, respBody
2175 | }
2176 |
```