mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-10 07:58:12 -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/postgreslistavailableextensions"
|
||||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistinstalledextensions"
|
_ "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/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/postgres/postgressql"
|
||||||
_ "github.com/googleapis/genai-toolbox/internal/tools/redis"
|
_ "github.com/googleapis/genai-toolbox/internal/tools/redis"
|
||||||
_ "github.com/googleapis/genai-toolbox/internal/tools/serverlessspark/serverlesssparkgetbatch"
|
_ "github.com/googleapis/genai-toolbox/internal/tools/serverlessspark/serverlesssparkgetbatch"
|
||||||
|
|||||||
@@ -1397,7 +1397,7 @@ func TestPrebuiltTools(t *testing.T) {
|
|||||||
wantToolset: server.ToolsetConfigs{
|
wantToolset: server.ToolsetConfigs{
|
||||||
"alloydb_postgres_database_tools": tools.ToolsetConfig{
|
"alloydb_postgres_database_tools": tools.ToolsetConfig{
|
||||||
Name: "alloydb_postgres_database_tools",
|
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{
|
wantToolset: server.ToolsetConfigs{
|
||||||
"cloud_sql_postgres_database_tools": tools.ToolsetConfig{
|
"cloud_sql_postgres_database_tools": tools.ToolsetConfig{
|
||||||
Name: "cloud_sql_postgres_database_tools",
|
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{
|
wantToolset: server.ToolsetConfigs{
|
||||||
"postgres_database_tools": tools.ToolsetConfig{
|
"postgres_database_tools": tools.ToolsetConfig{
|
||||||
Name: "postgres_database_tools",
|
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_replication_slots`: Lists replication slots in the database.
|
||||||
* `list_invalid_indexes`: Lists invalid indexes in the database.
|
* `list_invalid_indexes`: Lists invalid indexes in the database.
|
||||||
* `get_query_plan`: Generate the execution plan of a statement.
|
* `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
|
## 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_replication_slots`: Lists replication slots in the database.
|
||||||
* `list_invalid_indexes`: Lists invalid indexes in the database.
|
* `list_invalid_indexes`: Lists invalid indexes in the database.
|
||||||
* `get_query_plan`: Generate the execution plan of a statement.
|
* `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
|
## 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_replication_slots`: Lists replication slots in the database.
|
||||||
* `list_invalid_indexes`: Lists invalid indexes in the database.
|
* `list_invalid_indexes`: Lists invalid indexes in the database.
|
||||||
* `get_query_plan`: Generate the execution plan of a statement.
|
* `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
|
## 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)
|
- [`postgres-list-installed-extensions`](../tools/postgres/postgres-list-installed-extensions.md)
|
||||||
List installed extensions in a PostgreSQL database.
|
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
|
### Pre-built Configurations
|
||||||
|
|
||||||
- [AlloyDB using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/alloydb_pg_mcp/)
|
- [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)
|
- [`postgres-list-installed-extensions`](../tools/postgres/postgres-list-installed-extensions.md)
|
||||||
List installed extensions in a PostgreSQL database.
|
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
|
### Pre-built Configurations
|
||||||
|
|
||||||
- [Cloud SQL for Postgres using
|
- [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)
|
- [`postgres-list-installed-extensions`](../tools/postgres/postgres-list-installed-extensions.md)
|
||||||
List installed extensions in a PostgreSQL database.
|
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
|
### Pre-built Configurations
|
||||||
|
|
||||||
- [PostgreSQL using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/postgres_mcp/)
|
- [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)."
|
description: "The SQL statement for which you want to generate plan (omit the EXPLAIN keyword)."
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
list_views:
|
||||||
|
kind: postgres-list-views
|
||||||
|
source: alloydb-pg-source
|
||||||
toolsets:
|
toolsets:
|
||||||
alloydb_postgres_database_tools:
|
alloydb_postgres_database_tools:
|
||||||
- execute_sql
|
- execute_sql
|
||||||
@@ -169,3 +172,4 @@ toolsets:
|
|||||||
- list_replication_slots
|
- list_replication_slots
|
||||||
- list_invalid_indexes
|
- list_invalid_indexes
|
||||||
- get_query_plan
|
- 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)."
|
description: "The SQL statement for which you want to generate plan (omit the EXPLAIN keyword)."
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
list_views:
|
||||||
|
kind: postgres-list-views
|
||||||
|
source: cloudsql-pg-source
|
||||||
|
|
||||||
toolsets:
|
toolsets:
|
||||||
cloud_sql_postgres_database_tools:
|
cloud_sql_postgres_database_tools:
|
||||||
- execute_sql
|
- execute_sql
|
||||||
@@ -168,3 +172,4 @@ toolsets:
|
|||||||
- list_replication_slots
|
- list_replication_slots
|
||||||
- list_invalid_indexes
|
- list_invalid_indexes
|
||||||
- get_query_plan
|
- 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)."
|
description: "The SQL statement for which you want to generate plan (omit the EXPLAIN keyword)."
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
list_views:
|
||||||
|
kind: postgres-list-views
|
||||||
|
source: postgresql-source
|
||||||
|
|
||||||
toolsets:
|
toolsets:
|
||||||
postgres_database_tools:
|
postgres_database_tools:
|
||||||
- execute_sql
|
- execute_sql
|
||||||
@@ -167,3 +171,4 @@ toolsets:
|
|||||||
- list_replication_slots
|
- list_replication_slots
|
||||||
- list_invalid_indexes
|
- list_invalid_indexes
|
||||||
- get_query_plan
|
- 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"
|
PostgresListActiveQueriesToolKind = "postgres-list-active-queries"
|
||||||
PostgresListInstalledExtensionsToolKind = "postgres-list-installed-extensions"
|
PostgresListInstalledExtensionsToolKind = "postgres-list-installed-extensions"
|
||||||
PostgresListAvailableExtensionsToolKind = "postgres-list-available-extensions"
|
PostgresListAvailableExtensionsToolKind = "postgres-list-available-extensions"
|
||||||
|
PostgresListViewsToolKind = "postgres-list-views"
|
||||||
PostgresDatabase = os.Getenv("POSTGRES_DATABASE")
|
PostgresDatabase = os.Getenv("POSTGRES_DATABASE")
|
||||||
PostgresHost = os.Getenv("POSTGRES_HOST")
|
PostgresHost = os.Getenv("POSTGRES_HOST")
|
||||||
PostgresPort = os.Getenv("POSTGRES_PORT")
|
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.",
|
"description": "Lists available extensions in the database.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tools["list_views"] = map[string]any{
|
||||||
|
"kind": PostgresListViewsToolKind,
|
||||||
|
"source": "my-instance",
|
||||||
|
}
|
||||||
|
|
||||||
config["tools"] = tools
|
config["tools"] = tools
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
@@ -189,6 +195,7 @@ func TestPostgres(t *testing.T) {
|
|||||||
|
|
||||||
// Run specific Postgres tool tests
|
// Run specific Postgres tool tests
|
||||||
runPostgresListTablesTest(t, tableNameParam, tableNameAuth)
|
runPostgresListTablesTest(t, tableNameParam, tableNameAuth)
|
||||||
|
runPostgresListViewsTest(t, ctx, pool, tableNameParam)
|
||||||
runPostgresListActiveQueriesTest(t, ctx, pool)
|
runPostgresListActiveQueriesTest(t, ctx, pool)
|
||||||
runPostgresListAvailableExtensionsTest(t)
|
runPostgresListAvailableExtensionsTest(t)
|
||||||
runPostgresListInstalledExtensionsTest(t)
|
runPostgresListInstalledExtensionsTest(t)
|
||||||
@@ -525,6 +532,94 @@ func runPostgresListActiveQueriesTest(t *testing.T, ctx context.Context, pool *p
|
|||||||
wg.Wait()
|
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) {
|
func runPostgresListAvailableExtensionsTest(t *testing.T) {
|
||||||
invokeTcs := []struct {
|
invokeTcs := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Reference in New Issue
Block a user