mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-11 08:28:11 -05:00
feat(tools/mysql-list-tables-missing-index): Add a new tool to list tables that do not have primary or unique keys in a MySQL instance (#1493)
## Description
---
A `mysql-list-tables-missing-index` tool searches tables that do not
have primary or unique indices in a MySQL database. It's compatible
with:
- cloud-sql-mysql
- mysql
`mysql-list-tables-missing-index` outputs table names, including
`table_schema` and `table_name` in JSON format. It takes 2 optional
input parameters:
- `table_schema` (optional): Only check tables in this specific
schema/database. Search all visible tables in all visible databases if
not specified.
- `limit` (optional): max number of queries to return, default `50`.
## Example
```yaml
tools:
list_tables_missing_index:
kind: mysql-list-tables-missing-index
source: my-mysql-instance
description: Find tables that do not have primary or unique key constraint. A primary key or unique key is the only mechanism that guaranttes a row is unique. Without them, the database-level protection against data integrity issues will be missing.
```
The response is a json array with the following fields:
```json
{
"table_schema": "the schema/database this table belongs to",
"table_name": "name of the table",
}
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
| kind | string | true | Must be "mysql-list-active-queries". |
| source | string | true | Name of the source the SQL should execute on.
|
| description | string | true | Description of the tool that is passed
to the LLM. |
## PR Checklist
---
> Thank you for opening a Pull Request! Before submitting your PR, there
are a
> few things you can do to make sure it goes smoothly:
- [x] Make sure you reviewed
[CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md)
- [x] Make sure to open an issue as a
[bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose)
before writing your code! That way we can discuss the change, evaluate
designs, and agree on the general idea
- [x] Ensure the tests and linter pass
- [x] Code coverage does not decrease (if any source code was changed)
- [x] Appropriate docs were updated (if necessary)
- [x] Make sure to add `!` if this involve a breaking change
---------
Co-authored-by: Averi Kitsch <akitsch@google.com>
This commit is contained in:
@@ -125,6 +125,7 @@ import (
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqllistactivequeries"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqllisttablefragmentation"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqllisttables"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqllisttablesmissinguniqueindexes"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqlsql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/neo4j/neo4jcypher"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/neo4j/neo4jexecutecypher"
|
||||
|
||||
@@ -1429,7 +1429,7 @@ func TestPrebuiltTools(t *testing.T) {
|
||||
wantToolset: server.ToolsetConfigs{
|
||||
"cloud_sql_mysql_database_tools": tools.ToolsetConfig{
|
||||
Name: "cloud_sql_mysql_database_tools",
|
||||
ToolNames: []string{"execute_sql", "list_tables", "get_query_plan", "list_active_queries", "list_table_fragmentation"},
|
||||
ToolNames: []string{"execute_sql", "list_tables", "get_query_plan", "list_active_queries", "list_tables_missing_unique_indexes", "list_table_fragmentation"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1469,7 +1469,7 @@ func TestPrebuiltTools(t *testing.T) {
|
||||
wantToolset: server.ToolsetConfigs{
|
||||
"mysql_database_tools": tools.ToolsetConfig{
|
||||
Name: "mysql_database_tools",
|
||||
ToolNames: []string{"execute_sql", "list_tables", "get_query_plan", "list_active_queries", "list_table_fragmentation"},
|
||||
ToolNames: []string{"execute_sql", "list_tables", "get_query_plan", "list_active_queries", "list_tables_missing_unique_indexes", "list_table_fragmentation"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -23,19 +23,22 @@ to a database by following these instructions][csql-mysql-quickstart].
|
||||
## Available Tools
|
||||
|
||||
- [`mysql-sql`](../tools/mysql/mysql-sql.md)
|
||||
Execute pre-defined prepared SQL queries in MySQL.
|
||||
Execute pre-defined prepared SQL queries in Cloud SQL for MySQL.
|
||||
|
||||
- [`mysql-execute-sql`](../tools/mysql/mysql-execute-sql.md)
|
||||
Run parameterized SQL queries in Cloud SQL for MySQL.
|
||||
|
||||
- [`mysql-list-active-queries`](../tools/mysql/mysql-list-active-queries.md)
|
||||
List active queries in MySQL.
|
||||
List active queries in Cloud SQL for MySQL.
|
||||
|
||||
- [`mysql-list-tables`](../tools/mysql/mysql-list-tables.md)
|
||||
List tables in a Cloud SQL for MySQL database.
|
||||
|
||||
- [`mysql-list-tables-missing-unique-indexes`](../tools/mysql/mysql-list-tables-missing-unique-indexes.md)
|
||||
List tables in a Cloud SQL for MySQL database that do not have primary or unique indices.
|
||||
|
||||
- [`mysql-list-table-fragmentation`](../tools/mysql/mysql-list-table-fragmentation.md)
|
||||
List table fragmentation in MySQL tables.
|
||||
List table fragmentation in Cloud SQL for MySQL tables.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ reliability, performance, and ease of use.
|
||||
- [`mysql-list-tables`](../tools/mysql/mysql-list-tables.md)
|
||||
List tables in a MySQL database.
|
||||
|
||||
- [`mysql-list-tables-missing-unique-indexes`](../tools/mysql/mysql-list-tables-missing-unique-indexes.md)
|
||||
List tables in a MySQL database that do not have primary or unique indices.
|
||||
|
||||
- [`mysql-list-table-fragmentation`](../tools/mysql/mysql-list-table-fragmentation.md)
|
||||
List table fragmentation in MySQL tables.
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: "mysql-list-tables-missing-unique-indexes"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
A "mysql-list-tables-missing-unique-indexes" tool lists tables that do not have primary or unique indices in a MySQL instance.
|
||||
aliases:
|
||||
- /resources/tools/mysql-list-tables-missing-unique-indexes
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
A `mysql-list-tables-missing-unique-indexes` tool searches tables that do not have primary or unique indices in a MySQL database. It's compatible with:
|
||||
|
||||
- [cloud-sql-mysql](../../sources/cloud-sql-mysql.md)
|
||||
- [mysql](../../sources/mysql.md)
|
||||
|
||||
`mysql-list-tables-missing-unique-indexes` outputs table names, including `table_schema` and `table_name` in JSON format. It takes 2 optional input parameters:
|
||||
|
||||
- `table_schema` (optional): Only check tables in this specific schema/database. Search all visible tables in all visible databases if not specified.
|
||||
- `limit` (optional): max number of queries to return, default `50`.
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
list_tables_missing_unique_indexes:
|
||||
kind: mysql-list-tables-missing-unique-indexes
|
||||
source: my-mysql-instance
|
||||
description: Find tables that do not have primary or unique key constraint. A primary key or unique key is the only mechanism that guaranttes a row is unique. Without them, the database-level protection against data integrity issues will be missing.
|
||||
```
|
||||
The response is a json array with the following fields:
|
||||
```json
|
||||
{
|
||||
"table_schema": "the schema/database this table belongs to",
|
||||
"table_name": "name of the table",
|
||||
}
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be "mysql-list-active-queries". |
|
||||
| source | string | true | Name of the source the SQL should execute on. |
|
||||
| description | string | true | Description of the tool that is passed to the LLM. |
|
||||
@@ -46,6 +46,10 @@ tools:
|
||||
kind: mysql-list-tables
|
||||
source: cloud-sql-mysql-source
|
||||
description: "Lists detailed schema information (object type, columns, constraints, indexes, triggers, comment) as JSON for user-created tables (ordinary or partitioned). Filters by a comma-separated list of names. If names are omitted, lists all tables in user schemas."
|
||||
list_tables_missing_unique_indexes:
|
||||
kind: mysql-list-tables-missing-unique-indexes
|
||||
source: cloud-sql-mysql-source
|
||||
description: "Find tables that do not have primary or unique key constraint. A primary key or unique key is the only mechanism that guaranttes a row is unique. Without them, the database-level protection against data integrity issues will be missing."
|
||||
list_table_fragmentation:
|
||||
kind: mysql-list-table-fragmentation
|
||||
source: cloud-sql-mysql-source
|
||||
@@ -57,4 +61,5 @@ toolsets:
|
||||
- list_tables
|
||||
- get_query_plan
|
||||
- list_active_queries
|
||||
- list_tables_missing_unique_indexes
|
||||
- list_table_fragmentation
|
||||
|
||||
@@ -50,6 +50,10 @@ tools:
|
||||
kind: mysql-list-tables
|
||||
source: mysql-source
|
||||
description: "Lists detailed schema information (object type, columns, constraints, indexes, triggers, comment) as JSON for user-created tables (ordinary or partitioned). Filters by a comma-separated list of names. If names are omitted, lists all tables in user schemas."
|
||||
list_tables_missing_unique_indexes:
|
||||
kind: mysql-list-tables-missing-unique-indexes
|
||||
source: mysql-source
|
||||
description: "Find tables that do not have primary or unique key constraint. A primary key or unique key is the only mechanism that guaranttes a row is unique. Without them, the database-level protection against data integrity issues will be missing."
|
||||
list_table_fragmentation:
|
||||
kind: mysql-list-table-fragmentation
|
||||
source: mysql-source
|
||||
@@ -61,4 +65,5 @@ toolsets:
|
||||
- list_tables
|
||||
- get_query_plan
|
||||
- list_active_queries
|
||||
- list_tables_missing_unique_indexes
|
||||
- list_table_fragmentation
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mysqllisttablesmissinguniqueindexes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources/cloudsqlmysql"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources/mysql"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqlcommon"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
)
|
||||
|
||||
const kind string = "mysql-list-tables-missing-unique-indexes"
|
||||
|
||||
const listTablesMissingUniqueIndexesStatement = `
|
||||
SELECT
|
||||
tab.table_schema AS table_schema,
|
||||
tab.table_name AS table_name
|
||||
FROM
|
||||
information_schema.tables tab
|
||||
LEFT JOIN
|
||||
information_schema.table_constraints tco
|
||||
ON
|
||||
tab.table_schema = tco.table_schema
|
||||
AND tab.table_name = tco.table_name
|
||||
AND tco.constraint_type IN ('PRIMARY KEY', 'UNIQUE')
|
||||
WHERE
|
||||
tco.constraint_type IS NULL
|
||||
AND tab.table_schema NOT IN('mysql', 'information_schema', 'performance_schema', 'sys')
|
||||
AND tab.table_type = 'BASE TABLE'
|
||||
AND (COALESCE(?, '') = '' OR tab.table_schema = ?)
|
||||
ORDER BY
|
||||
tab.table_schema,
|
||||
tab.table_name
|
||||
LIMIT ?;
|
||||
`
|
||||
|
||||
func init() {
|
||||
if !tools.Register(kind, newConfig) {
|
||||
panic(fmt.Sprintf("tool kind %q already registered", kind))
|
||||
}
|
||||
}
|
||||
|
||||
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
|
||||
actual := Config{Name: name}
|
||||
if err := decoder.DecodeContext(ctx, &actual); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return actual, nil
|
||||
}
|
||||
|
||||
type compatibleSource interface {
|
||||
MySQLPool() *sql.DB
|
||||
}
|
||||
|
||||
// validate compatible sources are still compatible
|
||||
var _ compatibleSource = &mysql.Source{}
|
||||
var _ compatibleSource = &cloudsqlmysql.Source{}
|
||||
|
||||
var compatibleSources = [...]string{mysql.SourceKind, cloudsqlmysql.SourceKind}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
}
|
||||
|
||||
// validate interface
|
||||
var _ tools.ToolConfig = Config{}
|
||||
|
||||
func (cfg Config) ToolConfigKind() string {
|
||||
return kind
|
||||
}
|
||||
|
||||
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
|
||||
// verify source exists
|
||||
rawS, ok := srcs[cfg.Source]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no source named %q configured", cfg.Source)
|
||||
}
|
||||
|
||||
// verify the source is compatible
|
||||
s, ok := rawS.(compatibleSource)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
|
||||
}
|
||||
|
||||
allParameters := tools.Parameters{
|
||||
tools.NewStringParameterWithDefault("table_schema", "", "(Optional) The database where the check is to be performed. Check all tables visible to the current user if not specified"),
|
||||
tools.NewIntParameterWithDefault("limit", 50, "(Optional) Max rows to return, default is 50"),
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: allParameters.McpManifest(),
|
||||
}
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Pool: s.MySQLPool(),
|
||||
allParams: allParameters,
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: allParameters.Manifest(), AuthRequired: cfg.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// validate interface
|
||||
var _ tools.Tool = Tool{}
|
||||
|
||||
type Tool struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
allParams tools.Parameters `yaml:"parameters"`
|
||||
Pool *sql.DB
|
||||
manifest tools.Manifest
|
||||
mcpManifest tools.McpManifest
|
||||
}
|
||||
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) {
|
||||
paramsMap := params.AsMap()
|
||||
|
||||
table_schema, ok := paramsMap["table_schema"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid 'table_schema' parameter; expected a string")
|
||||
}
|
||||
limit, ok := paramsMap["limit"].(int)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid 'limit' parameter; expected an integer")
|
||||
}
|
||||
|
||||
// Log the query executed for debugging.
|
||||
logger, err := util.LoggerFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting logger: %s", err)
|
||||
}
|
||||
logger.DebugContext(ctx, "executing `%s` tool query: %s", kind, listTablesMissingUniqueIndexesStatement)
|
||||
|
||||
results, err := t.Pool.QueryContext(ctx, listTablesMissingUniqueIndexesStatement, table_schema, table_schema, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to execute query: %w", err)
|
||||
}
|
||||
defer results.Close()
|
||||
|
||||
cols, err := results.Columns()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to retrieve rows column name: %w", err)
|
||||
}
|
||||
|
||||
// create an array of values for each column, which can be re-used to scan each row
|
||||
rawValues := make([]any, len(cols))
|
||||
values := make([]any, len(cols))
|
||||
for i := range rawValues {
|
||||
values[i] = &rawValues[i]
|
||||
}
|
||||
|
||||
colTypes, err := results.ColumnTypes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get column types: %w", err)
|
||||
}
|
||||
|
||||
var out []any
|
||||
for results.Next() {
|
||||
err := results.Scan(values...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse row: %w", err)
|
||||
}
|
||||
vMap := make(map[string]any)
|
||||
for i, name := range cols {
|
||||
val := rawValues[i]
|
||||
if val == nil {
|
||||
vMap[name] = nil
|
||||
continue
|
||||
}
|
||||
|
||||
vMap[name], err = mysqlcommon.ConvertToType(colTypes[i], val)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("errors encountered when converting values: %w", err)
|
||||
}
|
||||
}
|
||||
out = append(out, vMap)
|
||||
}
|
||||
|
||||
if err := results.Err(); err != nil {
|
||||
return nil, fmt.Errorf("errors encountered during row iteration: %w", err)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
|
||||
return tools.ParseParams(t.allParams, data, claims)
|
||||
}
|
||||
|
||||
func (t Tool) Manifest() tools.Manifest {
|
||||
return t.manifest
|
||||
}
|
||||
|
||||
func (t Tool) McpManifest() tools.McpManifest {
|
||||
return t.mcpManifest
|
||||
}
|
||||
|
||||
func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
|
||||
}
|
||||
|
||||
func (t Tool) RequiresClientAuthorization() bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mysqllisttablesmissinguniqueindexes_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/googleapis/genai-toolbox/internal/server"
|
||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqllisttablesmissinguniqueindexes"
|
||||
)
|
||||
|
||||
func TestParseFromYamlExecuteSql(t *testing.T) {
|
||||
ctx, err := testutils.ContextWithNewLogger()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
tcs := []struct {
|
||||
desc string
|
||||
in string
|
||||
want server.ToolConfigs
|
||||
}{
|
||||
{
|
||||
desc: "basic example",
|
||||
in: `
|
||||
tools:
|
||||
example_tool:
|
||||
kind: mysql-list-tables-missing-unique-indexes
|
||||
source: my-instance
|
||||
description: some description
|
||||
authRequired:
|
||||
- my-google-auth-service
|
||||
- other-auth-service
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"example_tool": mysqllisttablesmissinguniqueindexes.Config{
|
||||
Name: "example_tool",
|
||||
Kind: "mysql-list-tables-missing-unique-indexes",
|
||||
Source: "my-instance",
|
||||
Description: "some description",
|
||||
AuthRequired: []string{"my-google-auth-service", "other-auth-service"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := struct {
|
||||
Tools server.ToolConfigs `yaml:"tools"`
|
||||
}{}
|
||||
// Parse contents
|
||||
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to unmarshal: %s", err)
|
||||
}
|
||||
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
|
||||
t.Fatalf("incorrect parse: diff %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -313,6 +313,11 @@ func AddMySQLPrebuiltToolConfig(t *testing.T, config map[string]any) map[string]
|
||||
"source": "my-instance",
|
||||
"description": "Lists active queries in the database.",
|
||||
}
|
||||
tools["list_tables_missing_unique_indexes"] = map[string]any{
|
||||
"kind": "mysql-list-tables-missing-unique-indexes",
|
||||
"source": "my-instance",
|
||||
"description": "Lists tables that do not have primary or unique indexes in the database.",
|
||||
}
|
||||
tools["list_table_fragmentation"] = map[string]any{
|
||||
"kind": "mysql-list-table-fragmentation",
|
||||
"source": "my-instance",
|
||||
|
||||
@@ -137,5 +137,6 @@ func TestMySQLToolEndpoints(t *testing.T) {
|
||||
// Run specific MySQL tool tests
|
||||
tests.RunMySQLListTablesTest(t, MySQLDatabase, tableNameParam, tableNameAuth)
|
||||
tests.RunMySQLListActiveQueriesTest(t, ctx, pool)
|
||||
tests.RunMySQLListTablesMissingUniqueIndexes(t, ctx, pool, MySQLDatabase);
|
||||
tests.RunMySQLListTableFragmentationTest(t, MySQLDatabase, tableNameParam, tableNameAuth)
|
||||
}
|
||||
|
||||
232
tests/tool.go
232
tests/tool.go
@@ -1438,6 +1438,238 @@ func RunMySQLListActiveQueriesTest(t *testing.T, ctx context.Context, pool *sql.
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func RunMySQLListTablesMissingUniqueIndexes(t *testing.T, ctx context.Context, pool *sql.DB, databaseName string) {
|
||||
type listDetails struct {
|
||||
TableSchema string `json:"table_schema"`
|
||||
TableName string `json:"table_name"`
|
||||
}
|
||||
|
||||
// bunch of wanted
|
||||
nonUniqueKeyTableName := "t03_non_unqiue_key_table"
|
||||
noKeyTableName := "t04_no_key_table"
|
||||
nonUniqueKeyTableWant := listDetails{
|
||||
TableSchema: databaseName,
|
||||
TableName: nonUniqueKeyTableName,
|
||||
}
|
||||
noKeyTableWant := listDetails{
|
||||
TableSchema: databaseName,
|
||||
TableName: noKeyTableName,
|
||||
}
|
||||
|
||||
invokeTcs := []struct {
|
||||
name string
|
||||
requestBody io.Reader
|
||||
newTableName string
|
||||
newTablePrimaryKey bool
|
||||
newTableUniqueKey bool
|
||||
newTableNonUniqueKey bool
|
||||
wantStatusCode int
|
||||
want any
|
||||
}{
|
||||
{
|
||||
name: "invoke list_tables_missing_unique_indexes when nothing to be found",
|
||||
requestBody: bytes.NewBufferString(`{}`),
|
||||
newTableName: "",
|
||||
newTablePrimaryKey: false,
|
||||
newTableUniqueKey: false,
|
||||
newTableNonUniqueKey: false,
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: []listDetails(nil),
|
||||
},
|
||||
{
|
||||
name: "invoke list_tables_missing_unique_indexes pk table will not show",
|
||||
requestBody: bytes.NewBufferString(`{}`),
|
||||
newTableName: "t01",
|
||||
newTablePrimaryKey: true,
|
||||
newTableUniqueKey: false,
|
||||
newTableNonUniqueKey: false,
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: []listDetails(nil),
|
||||
},
|
||||
{
|
||||
name: "invoke list_tables_missing_unique_indexes uk table will not show",
|
||||
requestBody: bytes.NewBufferString(`{}`),
|
||||
newTableName: "t02",
|
||||
newTablePrimaryKey: false,
|
||||
newTableUniqueKey: true,
|
||||
newTableNonUniqueKey: false,
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: []listDetails(nil),
|
||||
},
|
||||
{
|
||||
name: "invoke list_tables_missing_unique_indexes non-unique key only table will show",
|
||||
requestBody: bytes.NewBufferString(`{}`),
|
||||
newTableName: nonUniqueKeyTableName,
|
||||
newTablePrimaryKey: false,
|
||||
newTableUniqueKey: false,
|
||||
newTableNonUniqueKey: true,
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: []listDetails{nonUniqueKeyTableWant},
|
||||
},
|
||||
{
|
||||
name: "invoke list_tables_missing_unique_indexes table with no key at all will show",
|
||||
requestBody: bytes.NewBufferString(`{}`),
|
||||
newTableName: noKeyTableName,
|
||||
newTablePrimaryKey: false,
|
||||
newTableUniqueKey: false,
|
||||
newTableNonUniqueKey: false,
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: []listDetails{nonUniqueKeyTableWant, noKeyTableWant},
|
||||
},
|
||||
{
|
||||
name: "invoke list_tables_missing_unique_indexes table w/ both pk & uk will not show",
|
||||
requestBody: bytes.NewBufferString(`{}`),
|
||||
newTableName: "t05",
|
||||
newTablePrimaryKey: true,
|
||||
newTableUniqueKey: true,
|
||||
newTableNonUniqueKey: false,
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: []listDetails{nonUniqueKeyTableWant, noKeyTableWant},
|
||||
},
|
||||
{
|
||||
name: "invoke list_tables_missing_unique_indexes table w/ uk & nk will not show",
|
||||
requestBody: bytes.NewBufferString(`{}`),
|
||||
newTableName: "t06",
|
||||
newTablePrimaryKey: false,
|
||||
newTableUniqueKey: true,
|
||||
newTableNonUniqueKey: true,
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: []listDetails{nonUniqueKeyTableWant, noKeyTableWant},
|
||||
},
|
||||
{
|
||||
name: "invoke list_tables_missing_unique_indexes table w/ pk & nk will not show",
|
||||
requestBody: bytes.NewBufferString(`{}`),
|
||||
newTableName: "t07",
|
||||
newTablePrimaryKey: true,
|
||||
newTableUniqueKey: false,
|
||||
newTableNonUniqueKey: true,
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: []listDetails{nonUniqueKeyTableWant, noKeyTableWant},
|
||||
},
|
||||
{
|
||||
name: "invoke list_tables_missing_unique_indexes with a non-exist database, nothing to show",
|
||||
requestBody: bytes.NewBufferString(`{"table_schema": "non-exist-database"}`),
|
||||
newTableName: "",
|
||||
newTablePrimaryKey: false,
|
||||
newTableUniqueKey: false,
|
||||
newTableNonUniqueKey: false,
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: []listDetails(nil),
|
||||
},
|
||||
{
|
||||
name: "invoke list_tables_missing_unique_indexes with the right database, show everything",
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf(`{"table_schema": "%s"}`, databaseName)),
|
||||
newTableName: "",
|
||||
newTablePrimaryKey: false,
|
||||
newTableUniqueKey: false,
|
||||
newTableNonUniqueKey: false,
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: []listDetails{nonUniqueKeyTableWant, noKeyTableWant},
|
||||
},
|
||||
{
|
||||
name: "invoke list_tables_missing_unique_indexes with limited output",
|
||||
requestBody: bytes.NewBufferString(`{"limit": 1}`),
|
||||
newTableName: "",
|
||||
newTablePrimaryKey: false,
|
||||
newTableUniqueKey: false,
|
||||
newTableNonUniqueKey: false,
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: []listDetails{nonUniqueKeyTableWant},
|
||||
},
|
||||
}
|
||||
|
||||
createTableHelper := func(t *testing.T, tableName, databaseName string, primaryKey, uniqueKey, nonUniqueKey bool, ctx context.Context, pool *sql.DB) func() {
|
||||
var stmt strings.Builder
|
||||
stmt.WriteString(fmt.Sprintf("CREATE TABLE %s (", tableName))
|
||||
stmt.WriteString("c1 INT")
|
||||
if primaryKey {
|
||||
stmt.WriteString(" PRIMARY KEY")
|
||||
}
|
||||
stmt.WriteString(", c2 INT, c3 CHAR(8)")
|
||||
if uniqueKey {
|
||||
stmt.WriteString(", UNIQUE(c2)")
|
||||
}
|
||||
if nonUniqueKey {
|
||||
stmt.WriteString(", INDEX(c3)")
|
||||
}
|
||||
stmt.WriteString(")")
|
||||
|
||||
t.Logf("Creating table: %s", stmt.String())
|
||||
if _, err := pool.ExecContext(ctx, stmt.String()); err != nil {
|
||||
t.Fatalf("failed executing %s: %v", stmt.String(), err)
|
||||
}
|
||||
|
||||
return func() {
|
||||
t.Logf("Dropping table: %s", tableName)
|
||||
if _, err := pool.ExecContext(ctx, fmt.Sprintf("DROP TABLE %s", tableName)); err != nil {
|
||||
t.Errorf("failed to drop table %s: %v", tableName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cleanups []func()
|
||||
defer func() {
|
||||
for i := len(cleanups) - 1; i >= 0; i-- {
|
||||
cleanups[i]()
|
||||
}
|
||||
}()
|
||||
|
||||
for _, tc := range invokeTcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.newTableName != "" {
|
||||
cleanup := createTableHelper(t, tc.newTableName, databaseName, tc.newTablePrimaryKey, tc.newTableUniqueKey, tc.newTableNonUniqueKey, ctx, pool)
|
||||
cleanups = append(cleanups, cleanup)
|
||||
}
|
||||
|
||||
const api = "http://127.0.0.1:5000/api/tool/list_tables_missing_unique_indexes/invoke"
|
||||
req, err := http.NewRequest(http.MethodPost, api, tc.requestBody)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create request: %v", err)
|
||||
}
|
||||
req.Header.Add("Content-type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to send request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != tc.wantStatusCode {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("wrong status code: got %d, want %d, body: %s", resp.StatusCode, tc.wantStatusCode, string(body))
|
||||
}
|
||||
if tc.wantStatusCode != http.StatusOK {
|
||||
return
|
||||
}
|
||||
|
||||
var bodyWrapper struct {
|
||||
Result json.RawMessage `json:"result"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&bodyWrapper); err != nil {
|
||||
t.Fatalf("error decoding response wrapper: %v", err)
|
||||
}
|
||||
|
||||
var resultString string
|
||||
if err := json.Unmarshal(bodyWrapper.Result, &resultString); err != nil {
|
||||
resultString = string(bodyWrapper.Result)
|
||||
}
|
||||
|
||||
var got any
|
||||
var details []listDetails
|
||||
if err := json.Unmarshal([]byte(resultString), &details); err != nil {
|
||||
t.Fatalf("failed to unmarshal nested listDetails string: %v", err)
|
||||
}
|
||||
got = details
|
||||
|
||||
if diff := cmp.Diff(tc.want, got, cmp.Comparer(func(a, b listDetails) bool {
|
||||
return a.TableSchema == b.TableSchema && a.TableName == b.TableName
|
||||
})); diff != "" {
|
||||
t.Errorf("Unexpected result: got %#v, want: %#v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func RunMySQLListTableFragmentationTest(t *testing.T, databaseName, tableNameParam, tableNameAuth string) {
|
||||
type tableFragmentationDetails struct {
|
||||
TableSchema string `json:"table_schema"`
|
||||
|
||||
Reference in New Issue
Block a user