mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-10 07:58:12 -05:00
feat(tools/mysql-list-table-fragmentation): Add a new tool to list table fragmentation in a MySQL instance (#1479)
## Description
A `mysql-list-table-fragmentation` tool checks table fragmentation of
MySQL tables by calculating the size of the data and index files in
bytes and comparing with free space allocated to each table. This tool
calculates `fragmentation_percentage` which represents the proportion of
free space relative to the total data and index size. It's compatible
with
- cloud-sql-mysql
- mysql
`mysql-list-table-fragmentation` outputs detailed information as JSON ,
ordered by the fragmentation percentage in descending order.
This tool takes 4 optional input parameters:
- `table_schema` (optional): The database where fragmentation check is
to be executed. Check all tables visible to the current user if not
specified.
- `table_name` (optional): Name of the table to be checked. Check all
tables visible to the current user if not specified.
- `data_free_threshold_bytes` (optional): Only show tables with at least
this much free space in bytes. Default 1.
- `limit` (optional): Max rows to return, default 10.
## Example
```yaml
tools:
list_table_fragmentation:
kind: mysql-list-table-fragmentation
source: my-mysql-instance
description: List table fragmentation in MySQL, by calculating the size of the data and index files and free space allocated to each table. The query calculates fragmentation percentage which represents the proportion of free space relative to the total data and index size. Storage can be reclaimed for tables with high fragmentation using OPTIMIZE TABLE.
```
The response is a json array with the following fields:
```json
{
"table_schema": "The schema/database this table belongs to",
"table_name": "Name of this table",
"data_size": "Size of the table data in bytes",
"index_size": "Size of the table's indexes in bytes",
"data_free": "Free space (bytes) available in the table's data file",
"fragmentation_percentage": "How much fragementation this table has",
}
```
## 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: Yuan Teoh <45984206+Yuan325@users.noreply.github.com>
Co-authored-by: Averi Kitsch <akitsch@google.com>
This commit is contained in:
@@ -123,6 +123,7 @@ import (
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/mssql/mssqlsql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqlexecutesql"
|
||||
_ "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/mysqlsql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/neo4j/neo4jcypher"
|
||||
|
||||
@@ -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"},
|
||||
ToolNames: []string{"execute_sql", "list_tables", "get_query_plan", "list_active_queries", "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"},
|
||||
ToolNames: []string{"execute_sql", "list_tables", "get_query_plan", "list_active_queries", "list_table_fragmentation"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -34,6 +34,9 @@ to a database by following these instructions][csql-mysql-quickstart].
|
||||
- [`mysql-list-tables`](../tools/mysql/mysql-list-tables.md)
|
||||
List tables in a Cloud SQL for MySQL database.
|
||||
|
||||
- [`mysql-list-table-fragmentation`](../tools/mysql/mysql-list-table-fragmentation.md)
|
||||
List table fragmentation in MySQL tables.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
- [Cloud SQL for MySQL using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/cloud_sql_mysql_mcp/)
|
||||
|
||||
@@ -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-table-fragmentation`](../tools/mysql/mysql-list-table-fragmentation.md)
|
||||
List table fragmentation in MySQL tables.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Database User
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: "mysql-list-table-fragmentation"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
A "mysql-list-table-fragmentation" tool lists top N fragemented tables in MySQL.
|
||||
aliases:
|
||||
- /resources/tools/mysql-list-table-fragmentation
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
A `mysql-list-table-fragmentation` tool checks table fragmentation of MySQL tables by calculating the size of the data and index files in bytes and comparing with free space allocated to each table. This tool calculates `fragmentation_percentage` which represents the proportion of free space relative to the total data and index size. It's compatible with
|
||||
|
||||
- [cloud-sql-mysql](../../sources/cloud-sql-mysql.md)
|
||||
- [mysql](../../sources/mysql.md)
|
||||
|
||||
`mysql-list-table-fragmentation` outputs detailed information as JSON , ordered by the fragmentation percentage in descending order.
|
||||
This tool takes 4 optional input parameters:
|
||||
|
||||
- `table_schema` (optional): The database where fragmentation check is to be executed. Check all tables visible to the current user if not specified.
|
||||
- `table_name` (optional): Name of the table to be checked. Check all tables visible to the current user if not specified.
|
||||
- `data_free_threshold_bytes` (optional): Only show tables with at least this much free space in bytes. Default 1.
|
||||
- `limit` (optional): Max rows to return, default 10.
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
list_table_fragmentation:
|
||||
kind: mysql-list-table-fragmentation
|
||||
source: my-mysql-instance
|
||||
description: List table fragmentation in MySQL, by calculating the size of the data and index files and free space allocated to each table. The query calculates fragmentation percentage which represents the proportion of free space relative to the total data and index size. Storage can be reclaimed for tables with high fragmentation using OPTIMIZE TABLE.
|
||||
```
|
||||
The response is a json array with the following fields:
|
||||
```json
|
||||
{
|
||||
"table_schema": "The schema/database this table belongs to",
|
||||
"table_name": "Name of this table",
|
||||
"data_size": "Size of the table data in bytes",
|
||||
"index_size": "Size of the table's indexes in bytes",
|
||||
"data_free": "Free space (bytes) available in the table's data file",
|
||||
"fragmentation_percentage": "How much fragementation this table has",
|
||||
}
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be "mysql-list-table-fragmentation". |
|
||||
| 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_table_fragmentation:
|
||||
kind: mysql-list-table-fragmentation
|
||||
source: cloud-sql-mysql-source
|
||||
description: List table fragmentation in MySQL, by calculating the size of the data and index files and free space allocated to each table. The query calculates fragmentation percentage which represents the proportion of free space relative to the total data and index size. Storage can be reclaimed for tables with high fragmentation using OPTIMIZE TABLE.
|
||||
|
||||
toolsets:
|
||||
cloud_sql_mysql_database_tools:
|
||||
@@ -53,3 +57,4 @@ toolsets:
|
||||
- list_tables
|
||||
- get_query_plan
|
||||
- list_active_queries
|
||||
- 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_table_fragmentation:
|
||||
kind: mysql-list-table-fragmentation
|
||||
source: mysql-source
|
||||
description: List table fragmentation in MySQL, by calculating the size of the data and index files and free space allocated to each table. The query calculates fragmentation percentage which represents the proportion of free space relative to the total data and index size. Storage can be reclaimed for tables with high fragmentation using OPTIMIZE TABLE.
|
||||
|
||||
toolsets:
|
||||
mysql_database_tools:
|
||||
@@ -57,3 +61,4 @@ toolsets:
|
||||
- list_tables
|
||||
- get_query_plan
|
||||
- list_active_queries
|
||||
- list_table_fragmentation
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
// 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 mysqllisttablefragmentation
|
||||
|
||||
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-table-fragmentation"
|
||||
|
||||
const listTableFragmentationStatement = `
|
||||
SELECT
|
||||
table_schema,
|
||||
table_name,
|
||||
data_length AS data_size,
|
||||
index_length AS index_size,
|
||||
data_free AS data_free,
|
||||
ROUND((data_free / (data_length + index_length)) * 100, 2) AS fragmentation_percentage
|
||||
FROM
|
||||
information_schema.tables
|
||||
WHERE
|
||||
table_schema NOT IN ('sys', 'performance_schema', 'mysql', 'information_schema')
|
||||
AND (COALESCE(?, '') = '' OR table_schema = ?)
|
||||
AND (COALESCE(?, '') = '' OR table_name = ?)
|
||||
AND data_free >= ?
|
||||
ORDER BY
|
||||
fragmentation_percentage DESC,
|
||||
table_schema,
|
||||
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 fragmentation check is to be executed. Check all tables visible to the current user if not specified"),
|
||||
tools.NewStringParameterWithDefault("table_name", "", "(Optional) Name of the table to be checked. Check all tables visible to the current user if not specified."),
|
||||
tools.NewIntParameterWithDefault("data_free_threshold_bytes", 1, "(Optional) Only show tables with at least this much free space in bytes. Default is 1"),
|
||||
tools.NewIntParameterWithDefault("limit", 10, "(Optional) Max rows to return, default is 10"),
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
table_name, ok := paramsMap["table_name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid 'table_name' parameter; expected a string")
|
||||
}
|
||||
data_free_threshold_bytes, ok := paramsMap["data_free_threshold_bytes"].(int)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid 'data_free_threshold_bytes' parameter; expected an integer")
|
||||
}
|
||||
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, listTableFragmentationStatement)
|
||||
|
||||
results, err := t.Pool.QueryContext(ctx, listTableFragmentationStatement, table_schema, table_schema, table_name, table_name, data_free_threshold_bytes, 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 mysqllisttablefragmentation_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/mysqllisttablefragmentation"
|
||||
)
|
||||
|
||||
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-table-fragmentation
|
||||
source: my-instance
|
||||
description: some description
|
||||
authRequired:
|
||||
- my-google-auth-service
|
||||
- other-auth-service
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"example_tool": mysqllisttablefragmentation.Config{
|
||||
Name: "example_tool",
|
||||
Kind: "mysql-list-table-fragmentation",
|
||||
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_table_fragmentation"] = map[string]any{
|
||||
"kind": "mysql-list-table-fragmentation",
|
||||
"source": "my-instance",
|
||||
"description": "Lists table fragmentation in the database.",
|
||||
}
|
||||
config["tools"] = tools
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -137,4 +137,5 @@ func TestMySQLToolEndpoints(t *testing.T) {
|
||||
// Run specific MySQL tool tests
|
||||
tests.RunMySQLListTablesTest(t, MySQLDatabase, tableNameParam, tableNameAuth)
|
||||
tests.RunMySQLListActiveQueriesTest(t, ctx, pool)
|
||||
tests.RunMySQLListTableFragmentationTest(t, MySQLDatabase, tableNameParam, tableNameAuth)
|
||||
}
|
||||
|
||||
127
tests/tool.go
127
tests/tool.go
@@ -1438,6 +1438,133 @@ func RunMySQLListActiveQueriesTest(t *testing.T, ctx context.Context, pool *sql.
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func RunMySQLListTableFragmentationTest(t *testing.T, databaseName, tableNameParam, tableNameAuth string) {
|
||||
type tableFragmentationDetails struct {
|
||||
TableSchema string `json:"table_schema"`
|
||||
TableName string `json:"table_name"`
|
||||
DataSize any `json:"data_size"`
|
||||
IndexSize any `json:"index_size"`
|
||||
DataFree any `json:"data_free"`
|
||||
FragmentationPercentage any `json:"fragmentation_percentage"`
|
||||
}
|
||||
|
||||
paramTableEntryWanted := tableFragmentationDetails{
|
||||
TableSchema: databaseName,
|
||||
TableName: tableNameParam,
|
||||
DataSize: any(nil),
|
||||
IndexSize: any(nil),
|
||||
DataFree: any(nil),
|
||||
FragmentationPercentage: any(nil),
|
||||
}
|
||||
authTableEntryWanted := tableFragmentationDetails{
|
||||
TableSchema: databaseName,
|
||||
TableName: tableNameAuth,
|
||||
DataSize: any(nil),
|
||||
IndexSize: any(nil),
|
||||
DataFree: any(nil),
|
||||
FragmentationPercentage: any(nil),
|
||||
}
|
||||
|
||||
invokeTcs := []struct {
|
||||
name string
|
||||
requestBody io.Reader
|
||||
wantStatusCode int
|
||||
want any
|
||||
}{
|
||||
{
|
||||
name: "invoke list_table_fragmentation on all, no data_free threshold, expected to have 2 results",
|
||||
requestBody: bytes.NewBufferString(`{"data_free_threshold_bytes": 0}`),
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: []tableFragmentationDetails{authTableEntryWanted, paramTableEntryWanted},
|
||||
},
|
||||
{
|
||||
name: "invoke list_table_fragmentation on all, no data_free threshold, limit to 1, expected to have 1 results",
|
||||
requestBody: bytes.NewBufferString(`{"data_free_threshold_bytes": 0, "limit": 1}`),
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: []tableFragmentationDetails{authTableEntryWanted},
|
||||
},
|
||||
{
|
||||
name: "invoke list_table_fragmentation on all databases and 1 specific table name, no data_free threshold, expected to have 1 result",
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf(`{"table_name": "%s","data_free_threshold_bytes": 0}`, tableNameAuth)),
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: []tableFragmentationDetails{authTableEntryWanted},
|
||||
},
|
||||
{
|
||||
name: "invoke list_table_fragmentation on 1 database and 1 specific table name, no data_free threshold, expected to have 1 result",
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf(`{"table_schema": "%s", "table_name": "%s", "data_free_threshold_bytes": 0}`, databaseName, tableNameParam)),
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: []tableFragmentationDetails{paramTableEntryWanted},
|
||||
},
|
||||
{
|
||||
name: "invoke list_table_fragmentation on 1 database and 1 specific table name, high data_free threshold, expected to have 0 result",
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf(`{"table_schema": "%s", "table_name": "%s", "data_free_threshold_bytes": 1000000000}`, databaseName, tableNameParam)),
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: []tableFragmentationDetails(nil),
|
||||
},
|
||||
{
|
||||
name: "invoke list_table_fragmentation on 1 non-exist database, no data_free threshold, expected to have 0 result",
|
||||
requestBody: bytes.NewBufferString(`{"table_schema": "non_existent_database", "data_free_threshold_bytes": 0}`),
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: []tableFragmentationDetails(nil),
|
||||
},
|
||||
{
|
||||
name: "invoke list_table_fragmentation on 1 non-exist table, no data_free threshold, expected to have 0 result",
|
||||
requestBody: bytes.NewBufferString(`{"table_name": "non_existent_table", "data_free_threshold_bytes": 0}`),
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: []tableFragmentationDetails(nil),
|
||||
},
|
||||
}
|
||||
for _, tc := range invokeTcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
const api = "http://127.0.0.1:5000/api/tool/list_table_fragmentation/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 []tableFragmentationDetails
|
||||
if err := json.Unmarshal([]byte(resultString), &details); err != nil {
|
||||
t.Fatalf("failed to unmarshal outer JSON array into []tableInfo: %v", err)
|
||||
}
|
||||
got = details
|
||||
|
||||
if diff := cmp.Diff(tc.want, got, cmp.Comparer(func(a, b tableFragmentationDetails) bool {
|
||||
return a.TableSchema == b.TableSchema && a.TableName == b.TableName
|
||||
})); diff != "" {
|
||||
t.Errorf("Unexpected result: got %#v, want: %#v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// RunRequest is a helper function to send HTTP requests and return the response
|
||||
func RunRequest(t *testing.T, method, url string, body io.Reader, headers map[string]string) (*http.Response, []byte) {
|
||||
// Send request
|
||||
|
||||
Reference in New Issue
Block a user