#
tokens: 42622/50000 1/868 files (page 51/53)
lines: on (toggle) GitHub
raw markdown copy reset
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 | 
```
Page 51/53FirstPrevNextLast