mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-09 07:28:05 -05:00
feat(tools/postgres-list-views): add new postgres-list-views tool (#1709)
## Description Adds a read-only PostgreSQL custom list_views tool, that returns the details of views present in database. Each row includes: schema_name, view_name, owner_name Test Output: <img width="2433" height="1274" alt="Screenshot 2025-10-19 at 3 59 31 PM" src="https://github.com/user-attachments/assets/76e1e994-390a-4239-aba7-0d02253bbcc4" /> <img width="2533" height="1017" alt="Screenshot 2025-10-19 at 4 07 25 PM" src="https://github.com/user-attachments/assets/537fbddd-d862-4044-a09b-cb9f180c21fa" /> ## 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 🛠️ Fixes #1738 Co-authored-by: Averi Kitsch <akitsch@google.com>
This commit is contained in:
@@ -153,6 +153,7 @@ import (
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistavailableextensions"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistinstalledextensions"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslisttables"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistviews"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgressql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/redis"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/serverlessspark/serverlesssparkgetbatch"
|
||||
|
||||
@@ -1397,7 +1397,7 @@ func TestPrebuiltTools(t *testing.T) {
|
||||
wantToolset: server.ToolsetConfigs{
|
||||
"alloydb_postgres_database_tools": tools.ToolsetConfig{
|
||||
Name: "alloydb_postgres_database_tools",
|
||||
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan"},
|
||||
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1427,7 +1427,7 @@ func TestPrebuiltTools(t *testing.T) {
|
||||
wantToolset: server.ToolsetConfigs{
|
||||
"cloud_sql_postgres_database_tools": tools.ToolsetConfig{
|
||||
Name: "cloud_sql_postgres_database_tools",
|
||||
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan"},
|
||||
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1527,7 +1527,7 @@ func TestPrebuiltTools(t *testing.T) {
|
||||
wantToolset: server.ToolsetConfigs{
|
||||
"postgres_database_tools": tools.ToolsetConfig{
|
||||
Name: "postgres_database_tools",
|
||||
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan"},
|
||||
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -43,6 +43,8 @@ details on how to connect your AI tools (IDEs) to databases via Toolbox and MCP.
|
||||
* `list_replication_slots`: Lists replication slots in the database.
|
||||
* `list_invalid_indexes`: Lists invalid indexes in the database.
|
||||
* `get_query_plan`: Generate the execution plan of a statement.
|
||||
* `list_views`: Lists views in the database from pg_views with a default
|
||||
limit of 50 rows. Returns schemaname, viewname and the ownername.
|
||||
|
||||
## AlloyDB Postgres Admin
|
||||
|
||||
@@ -210,6 +212,8 @@ details on how to connect your AI tools (IDEs) to databases via Toolbox and MCP.
|
||||
* `list_replication_slots`: Lists replication slots in the database.
|
||||
* `list_invalid_indexes`: Lists invalid indexes in the database.
|
||||
* `get_query_plan`: Generate the execution plan of a statement.
|
||||
* `list_views`: Lists views in the database from pg_views with a default
|
||||
limit of 50 rows. Returns schemaname, viewname and the ownername.
|
||||
|
||||
## Cloud SQL for PostgreSQL Observability
|
||||
|
||||
@@ -492,6 +496,8 @@ details on how to connect your AI tools (IDEs) to databases via Toolbox and MCP.
|
||||
* `list_replication_slots`: Lists replication slots in the database.
|
||||
* `list_invalid_indexes`: Lists invalid indexes in the database.
|
||||
* `get_query_plan`: Generate the execution plan of a statement.
|
||||
* `list_views`: Lists views in the database from pg_views with a default
|
||||
limit of 50 rows. Returns schemaname, viewname and the ownername.
|
||||
|
||||
## Google Cloud Serverless for Apache Spark
|
||||
|
||||
|
||||
@@ -45,6 +45,9 @@ cluster][alloydb-free-trial].
|
||||
- [`postgres-list-installed-extensions`](../tools/postgres/postgres-list-installed-extensions.md)
|
||||
List installed extensions in a PostgreSQL database.
|
||||
|
||||
- [`postgres-list-views`](../tools/postgres/postgres-list-views.md)
|
||||
List views in an AlloyDB for PostgreSQL database.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
- [AlloyDB using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/alloydb_pg_mcp/)
|
||||
|
||||
@@ -41,6 +41,9 @@ to a database by following these instructions][csql-pg-quickstart].
|
||||
- [`postgres-list-installed-extensions`](../tools/postgres/postgres-list-installed-extensions.md)
|
||||
List installed extensions in a PostgreSQL database.
|
||||
|
||||
- [`postgres-list-views`](../tools/postgres/postgres-list-views.md)
|
||||
List views in a PostgreSQL database.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
- [Cloud SQL for Postgres using
|
||||
|
||||
@@ -35,6 +35,9 @@ reputation for reliability, feature robustness, and performance.
|
||||
- [`postgres-list-installed-extensions`](../tools/postgres/postgres-list-installed-extensions.md)
|
||||
List installed extensions in a PostgreSQL database.
|
||||
|
||||
- [`postgres-list-views`](../tools/postgres/postgres-list-views.md)
|
||||
List views in a PostgreSQL database.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
- [PostgreSQL using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/postgres_mcp/)
|
||||
|
||||
40
docs/en/resources/tools/postgres/postgres-list-views.md
Normal file
40
docs/en/resources/tools/postgres/postgres-list-views.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: "postgres-list-views"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
The "postgres-list-views" tool lists views in a Postgres database, with a default limit of 50 rows.
|
||||
aliases:
|
||||
- /resources/tools/postgres-list-views
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
The `postgres-list-views` tool retrieves a list of top N (default 50) views from a Postgres database, excluding those in system schemas (`pg_catalog`, `information_schema`). It's compatible with any of the following sources:
|
||||
|
||||
- [alloydb-postgres](../../sources/alloydb-pg.md)
|
||||
- [cloud-sql-postgres](../../sources/cloud-sql-pg.md)
|
||||
- [postgres](../../sources/postgres.md)
|
||||
|
||||
`postgres-list-views` lists detailed view information (schemaname, viewname, ownername) as JSON for views in a database. The tool takes the following input parameters:
|
||||
|
||||
- `viewname` (optional): A string pattern to filter view names. The search uses SQL
|
||||
LIKE operator to filter the views. Default: `""`
|
||||
- `limit` (optional): The maximum number of rows to return. Default: `50`.
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
list_views:
|
||||
kind: postgres-list-views
|
||||
source: cloudsql-pg-source
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|-------------|:--------:|:-------------:|------------------------------------------------------|
|
||||
| kind | string | true | Must be "postgres-list-views". |
|
||||
| source | string | true | Name of the source the SQL should execute on. |
|
||||
| description | string | false | Description of the tool that is passed to the agent. |
|
||||
@@ -156,6 +156,9 @@ tools:
|
||||
description: "The SQL statement for which you want to generate plan (omit the EXPLAIN keyword)."
|
||||
required: true
|
||||
|
||||
list_views:
|
||||
kind: postgres-list-views
|
||||
source: alloydb-pg-source
|
||||
toolsets:
|
||||
alloydb_postgres_database_tools:
|
||||
- execute_sql
|
||||
@@ -169,3 +172,4 @@ toolsets:
|
||||
- list_replication_slots
|
||||
- list_invalid_indexes
|
||||
- get_query_plan
|
||||
- list_views
|
||||
|
||||
@@ -155,6 +155,10 @@ tools:
|
||||
description: "The SQL statement for which you want to generate plan (omit the EXPLAIN keyword)."
|
||||
required: true
|
||||
|
||||
list_views:
|
||||
kind: postgres-list-views
|
||||
source: cloudsql-pg-source
|
||||
|
||||
toolsets:
|
||||
cloud_sql_postgres_database_tools:
|
||||
- execute_sql
|
||||
@@ -168,3 +172,4 @@ toolsets:
|
||||
- list_replication_slots
|
||||
- list_invalid_indexes
|
||||
- get_query_plan
|
||||
- list_views
|
||||
|
||||
@@ -154,6 +154,10 @@ tools:
|
||||
description: "The SQL statement for which you want to generate plan (omit the EXPLAIN keyword)."
|
||||
required: true
|
||||
|
||||
list_views:
|
||||
kind: postgres-list-views
|
||||
source: postgresql-source
|
||||
|
||||
toolsets:
|
||||
postgres_database_tools:
|
||||
- execute_sql
|
||||
@@ -167,3 +171,4 @@ toolsets:
|
||||
- list_replication_slots
|
||||
- list_invalid_indexes
|
||||
- get_query_plan
|
||||
- list_views
|
||||
|
||||
186
internal/tools/postgres/postgreslistviews/postgreslistviews.go
Normal file
186
internal/tools/postgres/postgreslistviews/postgreslistviews.go
Normal file
@@ -0,0 +1,186 @@
|
||||
// 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 postgreslistviews
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources/alloydbpg"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources/cloudsqlpg"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources/postgres"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
const kind string = "postgres-list-views"
|
||||
|
||||
const listViewsStatement = `
|
||||
SELECT schemaname, viewname, viewowner
|
||||
FROM pg_views
|
||||
WHERE
|
||||
schemaname NOT IN ('pg_catalog', 'information_schema')
|
||||
AND ($1::text IS NULL OR viewname LIKE '%' || $1::text || '%')
|
||||
ORDER BY viewname
|
||||
LIMIT COALESCE($2::int, 50);
|
||||
`
|
||||
|
||||
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 {
|
||||
PostgresPool() *pgxpool.Pool
|
||||
}
|
||||
|
||||
// validate compatible sources are still compatible
|
||||
var _ compatibleSource = &alloydbpg.Source{}
|
||||
var _ compatibleSource = &cloudsqlpg.Source{}
|
||||
var _ compatibleSource = &postgres.Source{}
|
||||
|
||||
var compatibleSources = [...]string{alloydbpg.SourceKind, cloudsqlpg.SourceKind, postgres.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"`
|
||||
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("viewname", "", "Optional: A specific view name to search for."),
|
||||
tools.NewIntParameterWithDefault("limit", 50, "Optional: The maximum number of rows to return."),
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Lists views in the database from pg_views with a default limit of 50 rows. Returns schemaname, viewname and the ownername."
|
||||
}
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
name: cfg.Name,
|
||||
kind: kind,
|
||||
authRequired: cfg.AuthRequired,
|
||||
allParams: allParameters,
|
||||
pool: s.PostgresPool(),
|
||||
manifest: tools.Manifest{
|
||||
Description: cfg.Description,
|
||||
Parameters: paramManifest,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
},
|
||||
mcpManifest: mcpManifest,
|
||||
}, 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:"allParams"`
|
||||
pool *pgxpool.Pool
|
||||
manifest tools.Manifest
|
||||
mcpManifest tools.McpManifest
|
||||
}
|
||||
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) {
|
||||
paramsMap := params.AsMap()
|
||||
|
||||
newParams, err := tools.GetParams(t.allParams, paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to extract standard params %w", err)
|
||||
}
|
||||
sliceParams := newParams.AsSlice()
|
||||
|
||||
results, err := t.pool.Query(ctx, listViewsStatement, sliceParams...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to execute query: %w", err)
|
||||
}
|
||||
defer results.Close()
|
||||
|
||||
fields := results.FieldDescriptions()
|
||||
var out []map[string]any
|
||||
|
||||
for results.Next() {
|
||||
values, err := results.Values()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse row: %w", err)
|
||||
}
|
||||
rowMap := make(map[string]any)
|
||||
for i, field := range fields {
|
||||
rowMap[string(field.Name)] = values[i]
|
||||
}
|
||||
out = append(out, rowMap)
|
||||
}
|
||||
|
||||
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,95 @@
|
||||
// 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 postgreslistviews_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/postgres/postgreslistviews"
|
||||
)
|
||||
|
||||
func TestParseFromYamlPostgresListViews(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: postgres-list-views
|
||||
source: my-postgres-instance
|
||||
description: some description
|
||||
authRequired:
|
||||
- my-google-auth-service
|
||||
- other-auth-service
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"example_tool": postgreslistviews.Config{
|
||||
Name: "example_tool",
|
||||
Kind: "postgres-list-views",
|
||||
Source: "my-postgres-instance",
|
||||
Description: "some description",
|
||||
AuthRequired: []string{"my-google-auth-service", "other-auth-service"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "basic example",
|
||||
in: `
|
||||
tools:
|
||||
example_tool:
|
||||
kind: postgres-list-views
|
||||
source: my-postgres-instance
|
||||
description: some description
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"example_tool": postgreslistviews.Config{
|
||||
Name: "example_tool",
|
||||
Kind: "postgres-list-views",
|
||||
Source: "my-postgres-instance",
|
||||
Description: "some description",
|
||||
AuthRequired: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -45,6 +45,7 @@ var (
|
||||
PostgresListActiveQueriesToolKind = "postgres-list-active-queries"
|
||||
PostgresListInstalledExtensionsToolKind = "postgres-list-installed-extensions"
|
||||
PostgresListAvailableExtensionsToolKind = "postgres-list-available-extensions"
|
||||
PostgresListViewsToolKind = "postgres-list-views"
|
||||
PostgresDatabase = os.Getenv("POSTGRES_DATABASE")
|
||||
PostgresHost = os.Getenv("POSTGRES_HOST")
|
||||
PostgresPort = os.Getenv("POSTGRES_PORT")
|
||||
@@ -104,6 +105,11 @@ func addPrebuiltToolConfig(t *testing.T, config map[string]any) map[string]any {
|
||||
"description": "Lists available extensions in the database.",
|
||||
}
|
||||
|
||||
tools["list_views"] = map[string]any{
|
||||
"kind": PostgresListViewsToolKind,
|
||||
"source": "my-instance",
|
||||
}
|
||||
|
||||
config["tools"] = tools
|
||||
return config
|
||||
}
|
||||
@@ -189,6 +195,7 @@ func TestPostgres(t *testing.T) {
|
||||
|
||||
// Run specific Postgres tool tests
|
||||
runPostgresListTablesTest(t, tableNameParam, tableNameAuth)
|
||||
runPostgresListViewsTest(t, ctx, pool, tableNameParam)
|
||||
runPostgresListActiveQueriesTest(t, ctx, pool)
|
||||
runPostgresListAvailableExtensionsTest(t)
|
||||
runPostgresListInstalledExtensionsTest(t)
|
||||
@@ -525,6 +532,94 @@ func runPostgresListActiveQueriesTest(t *testing.T, ctx context.Context, pool *p
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func setUpPostgresViews(t *testing.T, ctx context.Context, pool *pgxpool.Pool, viewName, tableName string) func() {
|
||||
createView := fmt.Sprintf("CREATE VIEW %s AS SELECT name FROM %s", viewName, tableName)
|
||||
_, err := pool.Exec(ctx, createView)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create view: %v", err)
|
||||
}
|
||||
return func() {
|
||||
dropView := fmt.Sprintf("DROP VIEW %s", viewName)
|
||||
_, err := pool.Exec(ctx, dropView)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to drop view: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runPostgresListViewsTest(t *testing.T, ctx context.Context, pool *pgxpool.Pool, tableName string) {
|
||||
viewName1 := "test_view_1" + strings.ReplaceAll(uuid.New().String(), "-", "")
|
||||
dropViewfunc1 := setUpPostgresViews(t, ctx, pool, viewName1, tableName)
|
||||
defer dropViewfunc1()
|
||||
|
||||
invokeTcs := []struct {
|
||||
name string
|
||||
requestBody io.Reader
|
||||
wantStatusCode int
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "invoke list_views with newly created view",
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"viewname": "%s"}`, viewName1))),
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: fmt.Sprintf(`[{"schemaname":"public","viewname":"%s","viewowner":"postgres"}]`, viewName1),
|
||||
},
|
||||
{
|
||||
name: "invoke list_views with non-existent_view",
|
||||
requestBody: bytes.NewBuffer([]byte(`{"viewname": "non_existent_view"}`)),
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: `null`,
|
||||
},
|
||||
}
|
||||
for _, tc := range invokeTcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
const api = "http://127.0.0.1:5000/api/tool/list_views/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, want any
|
||||
if err := json.Unmarshal([]byte(resultString), &got); err != nil {
|
||||
t.Fatalf("failed to unmarshal nested result string: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(tc.want), &want); err != nil {
|
||||
t.Fatalf("failed to unmarshal want string: %v", err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("Unexpected result (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runPostgresListAvailableExtensionsTest(t *testing.T) {
|
||||
invokeTcs := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user