mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-08 23:18:04 -05:00
feat: add tool to list store procedure (#2156)
Adds the following tools for Postgres: (1) list_stored_procedure: Retrieves stored procedure metadata returning schema name, procedure name, procedure owner, language, definition, and description, filtered by optional role name (procedure owner), schema name, and limit (default 20). <img width="3808" height="1181" alt="image" src="https://github.com/user-attachments/assets/43513a04-95ce-478f-a59f-3e5dafdb6b23" /> <img width="2654" height="1288" alt="image" src="https://github.com/user-attachments/assets/84aca162-3779-4daa-ae2f-61620560589f" /> > Should include a concise description of the changes (bug or feature), it's > impact, along with a summary of the solution ## 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
This commit is contained in:
@@ -198,6 +198,7 @@ import (
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistroles"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistschemas"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistsequences"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgresliststoredprocedure"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslisttables"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslisttablespaces"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslisttablestats"
|
||||
|
||||
@@ -1504,7 +1504,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", "list_views", "list_schemas", "database_overview", "list_triggers", "list_indexes", "list_sequences", "long_running_transactions", "list_locks", "replication_stats", "list_query_stats", "get_column_cardinality", "list_publication_tables", "list_tablespaces", "list_pg_settings", "list_database_stats", "list_roles", "list_table_stats"},
|
||||
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", "list_schemas", "database_overview", "list_triggers", "list_indexes", "list_sequences", "long_running_transactions", "list_locks", "replication_stats", "list_query_stats", "get_column_cardinality", "list_publication_tables", "list_tablespaces", "list_pg_settings", "list_database_stats", "list_roles", "list_table_stats", "list_stored_procedure"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1534,7 +1534,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", "list_views", "list_schemas", "database_overview", "list_triggers", "list_indexes", "list_sequences", "long_running_transactions", "list_locks", "replication_stats", "list_query_stats", "get_column_cardinality", "list_publication_tables", "list_tablespaces", "list_pg_settings", "list_database_stats", "list_roles", "list_table_stats"},
|
||||
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", "list_schemas", "database_overview", "list_triggers", "list_indexes", "list_sequences", "long_running_transactions", "list_locks", "replication_stats", "list_query_stats", "get_column_cardinality", "list_publication_tables", "list_tablespaces", "list_pg_settings", "list_database_stats", "list_roles", "list_table_stats", "list_stored_procedure"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1634,7 +1634,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", "list_views", "list_schemas", "database_overview", "list_triggers", "list_indexes", "list_sequences", "long_running_transactions", "list_locks", "replication_stats", "list_query_stats", "get_column_cardinality", "list_publication_tables", "list_tablespaces", "list_pg_settings", "list_database_stats", "list_roles", "list_table_stats"},
|
||||
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", "list_schemas", "database_overview", "list_triggers", "list_indexes", "list_sequences", "long_running_transactions", "list_locks", "replication_stats", "list_query_stats", "get_column_cardinality", "list_publication_tables", "list_tablespaces", "list_pg_settings", "list_database_stats", "list_roles", "list_table_stats", "list_stored_procedure"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -94,7 +94,10 @@ cluster][alloydb-free-trial].
|
||||
instance.
|
||||
|
||||
- [`postgres-list-roles`](../tools/postgres/postgres-list-roles.md)
|
||||
Lists all the user-created roles in PostgreSQL database..
|
||||
Lists all the user-created roles in PostgreSQL database.
|
||||
|
||||
- [`postgres-list-stored-procedure`](../tools/postgres/postgres-list-stored-procedure.md)
|
||||
Lists all the stored procedure in PostgreSQL database.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
|
||||
@@ -91,7 +91,10 @@ to a database by following these instructions][csql-pg-quickstart].
|
||||
instance.
|
||||
|
||||
- [`postgres-list-roles`](../tools/postgres/postgres-list-roles.md)
|
||||
Lists all the user-created roles in PostgreSQL database..
|
||||
Lists all the user-created roles in PostgreSQL database.
|
||||
|
||||
- [`postgres-list-stored-procedure`](../tools/postgres/postgres-list-stored-procedure.md)
|
||||
Lists all the stored procedure in PostgreSQL database.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
|
||||
@@ -85,7 +85,10 @@ reputation for reliability, feature robustness, and performance.
|
||||
server.
|
||||
|
||||
- [`postgres-list-roles`](../tools/postgres/postgres-list-roles.md)
|
||||
Lists all the user-created roles in PostgreSQL database..
|
||||
Lists all the user-created roles in PostgreSQL database.
|
||||
|
||||
- [`postgres-list-stored-procedure`](../tools/postgres/postgres-list-stored-procedure.md)
|
||||
Lists all the stored procedure in PostgreSQL database.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
title: "postgres-list-stored-procedure"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
The "postgres-list-stored-procedure" tool retrieves metadata for stored procedures in PostgreSQL, including procedure definitions, owners, languages, and descriptions.
|
||||
aliases:
|
||||
- /resources/tools/postgres-list-stored-procedure
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
The `postgres-list-stored-procedure` tool queries PostgreSQL system catalogs (`pg_proc`, `pg_namespace`, `pg_roles`, and `pg_language`) to retrieve comprehensive metadata about stored procedures in the database. It filters for procedures (kind = 'p') and provides the full procedure definition along with ownership and language information.
|
||||
|
||||
Compatible sources:
|
||||
|
||||
- [alloydb-postgres](../../sources/alloydb-pg.md)
|
||||
- [cloud-sql-postgres](../../sources/cloud-sql-pg.md)
|
||||
- [postgres](../../sources/postgres.md)
|
||||
|
||||
The tool returns a JSON array where each element represents a stored procedure with its schema, name, owner, language, complete definition, and optional description. Results are sorted by schema name and procedure name, with a default limit of 20 procedures.
|
||||
|
||||
## Parameters
|
||||
|
||||
| parameter | type | required | default | description |
|
||||
|--------------|---------|----------|---------|-------------|
|
||||
| role_name | string | false | null | Optional: The owner name to filter stored procedures by (supports partial matching) |
|
||||
| schema_name | string | false | null | Optional: The schema name to filter stored procedures by (supports partial matching) |
|
||||
| limit | integer | false | 20 | Optional: The maximum number of stored procedures to return |
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
list_stored_procedure:
|
||||
kind: postgres-list-stored-procedure
|
||||
source: postgres-source
|
||||
description: "Retrieves stored procedure metadata including definitions and owners."
|
||||
```
|
||||
|
||||
### Example Requests
|
||||
|
||||
**List all stored procedures (default limit 20):**
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
**Filter by specific owner (role):**
|
||||
```json
|
||||
{
|
||||
"role_name": "app_user"
|
||||
}
|
||||
```
|
||||
|
||||
**Filter by schema:**
|
||||
```json
|
||||
{
|
||||
"schema_name": "public"
|
||||
}
|
||||
```
|
||||
|
||||
**Filter by owner and schema with custom limit:**
|
||||
```json
|
||||
{
|
||||
"role_name": "postgres",
|
||||
"schema_name": "public",
|
||||
"limit": 50
|
||||
}
|
||||
```
|
||||
|
||||
**Filter by partial schema name:**
|
||||
```json
|
||||
{
|
||||
"schema_name": "audit"
|
||||
}
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"schema_name": "public",
|
||||
"name": "process_payment",
|
||||
"owner": "postgres",
|
||||
"language": "plpgsql",
|
||||
"definition": "CREATE OR REPLACE PROCEDURE public.process_payment(p_order_id integer, p_amount numeric)\n LANGUAGE plpgsql\nAS $procedure$\nBEGIN\n UPDATE orders SET status = 'paid', amount = p_amount WHERE id = p_order_id;\n INSERT INTO payment_log (order_id, amount, timestamp) VALUES (p_order_id, p_amount, now());\n COMMIT;\nEND\n$procedure$",
|
||||
"description": "Processes payment for an order and logs the transaction"
|
||||
},
|
||||
{
|
||||
"schema_name": "public",
|
||||
"name": "cleanup_old_records",
|
||||
"owner": "postgres",
|
||||
"language": "plpgsql",
|
||||
"definition": "CREATE OR REPLACE PROCEDURE public.cleanup_old_records(p_days_old integer)\n LANGUAGE plpgsql\nAS $procedure$\nDECLARE\n v_deleted integer;\nBEGIN\n DELETE FROM audit_logs WHERE created_at < now() - (p_days_old || ' days')::interval;\n GET DIAGNOSTICS v_deleted = ROW_COUNT;\n RAISE NOTICE 'Deleted % records', v_deleted;\nEND\n$procedure$",
|
||||
"description": "Removes audit log records older than specified days"
|
||||
},
|
||||
{
|
||||
"schema_name": "audit",
|
||||
"name": "audit_table_changes",
|
||||
"owner": "app_user",
|
||||
"language": "plpgsql",
|
||||
"definition": "CREATE OR REPLACE PROCEDURE audit.audit_table_changes()\n LANGUAGE plpgsql\nAS $procedure$\nBEGIN\n INSERT INTO audit.change_log (table_name, operation, changed_at) VALUES (TG_TABLE_NAME, TG_OP, now());\nEND\n$procedure$",
|
||||
"description": null
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Output Fields Reference
|
||||
|
||||
| field | type | description |
|
||||
|-------------|---------|-------------|
|
||||
| schema_name | string | Name of the schema containing the stored procedure. |
|
||||
| name | string | Name of the stored procedure. |
|
||||
| owner | string | PostgreSQL role/user who owns the stored procedure. |
|
||||
| language | string | Programming language in which the procedure is written (e.g., plpgsql, sql, c). |
|
||||
| definition | string | Complete SQL definition of the stored procedure, including the CREATE PROCEDURE statement. |
|
||||
| description | string | Optional description or comment for the procedure (may be null if no comment is set). |
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Code review and auditing**: Export procedure definitions for version control or compliance audits.
|
||||
- **Documentation generation**: Automatically extract procedure metadata and descriptions for documentation.
|
||||
- **Permission auditing**: Identify procedures owned by specific users or in specific schemas.
|
||||
- **Migration planning**: Retrieve all procedure definitions when planning database migrations.
|
||||
- **Dependency analysis**: Review procedure definitions to understand dependencies and call chains.
|
||||
- **Security assessment**: Audit which roles own and can modify stored procedures.
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- The tool filters at the database level using LIKE pattern matching, so partial matches are supported.
|
||||
- Procedure definitions can be large; consider using the `limit` parameter for large databases with many procedures.
|
||||
- Results are ordered by schema name and procedure name for consistent output.
|
||||
- The default limit of 20 procedures is suitable for most use cases; increase as needed.
|
||||
|
||||
## Notes
|
||||
|
||||
- Only stored **procedures** are returned; functions and other callable objects are excluded via the `prokind = 'p'` filter.
|
||||
- Filtering uses `LIKE` pattern matching, so filter values support partial matches (e.g., `role_name: "app"` will match "app_user", "app_admin", etc.).
|
||||
- The `definition` field contains the complete, runnable CREATE PROCEDURE statement.
|
||||
- The `description` field is populated from comments set via PostgreSQL's COMMENT command and may be null.
|
||||
@@ -224,6 +224,10 @@ tools:
|
||||
kind: postgres-list-roles
|
||||
source: alloydb-pg-source
|
||||
|
||||
list_stored_procedure:
|
||||
kind: postgres-list-stored-procedure
|
||||
source: alloydb-pg-source
|
||||
|
||||
toolsets:
|
||||
alloydb_postgres_database_tools:
|
||||
- execute_sql
|
||||
@@ -254,3 +258,4 @@ toolsets:
|
||||
- list_database_stats
|
||||
- list_roles
|
||||
- list_table_stats
|
||||
- list_stored_procedure
|
||||
|
||||
@@ -226,6 +226,10 @@ tools:
|
||||
kind: postgres-list-roles
|
||||
source: cloudsql-pg-source
|
||||
|
||||
list_stored_procedure:
|
||||
kind: postgres-list-stored-procedure
|
||||
source: cloudsql-pg-source
|
||||
|
||||
toolsets:
|
||||
cloud_sql_postgres_database_tools:
|
||||
- execute_sql
|
||||
@@ -256,3 +260,4 @@ toolsets:
|
||||
- list_database_stats
|
||||
- list_roles
|
||||
- list_table_stats
|
||||
- list_stored_procedure
|
||||
|
||||
@@ -225,6 +225,10 @@ tools:
|
||||
kind: postgres-list-roles
|
||||
source: postgresql-source
|
||||
|
||||
list_stored_procedure:
|
||||
kind: postgres-list-stored-procedure
|
||||
source: postgresql-source
|
||||
|
||||
toolsets:
|
||||
postgres_database_tools:
|
||||
- execute_sql
|
||||
@@ -255,3 +259,4 @@ toolsets:
|
||||
- list_database_stats
|
||||
- list_roles
|
||||
- list_table_stats
|
||||
- list_stored_procedure
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
// Copyright 2026 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 postgresliststoredprocedure
|
||||
|
||||
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/googleapis/genai-toolbox/internal/util/parameters"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
const kind string = "postgres-list-stored-procedure"
|
||||
|
||||
const listStoredProcedure = `
|
||||
SELECT
|
||||
n.nspname AS schema_name,
|
||||
p.proname AS name,
|
||||
r.rolname AS owner,
|
||||
l.lanname AS language,
|
||||
pg_catalog.pg_get_functiondef(p.oid) AS definition,
|
||||
pg_catalog.obj_description(p.oid, 'pg_proc') AS description
|
||||
FROM pg_catalog.pg_proc p
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
|
||||
JOIN pg_catalog.pg_roles r ON r.oid = p.proowner
|
||||
JOIN pg_catalog.pg_language l ON l.oid = p.prolang
|
||||
WHERE
|
||||
p.prokind = 'p' AND
|
||||
($1::text IS NULL OR r.rolname LIKE '%' || $1::text || '%') AND
|
||||
($2::text IS NULL OR n.nspname LIKE '%' || $2::text || '%')
|
||||
ORDER BY n.nspname, p.proname
|
||||
LIMIT
|
||||
COALESCE($3::int, 20);
|
||||
`
|
||||
|
||||
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 := parameters.Parameters{
|
||||
parameters.NewStringParameterWithRequired("role_name", "Optional: The owner name to filter the stored procedures by. Defaults to NULL.", false),
|
||||
parameters.NewStringParameterWithRequired("schema_name", "Optional: The schema name to filter the stored procedures by. Defaults to NULL.", false),
|
||||
parameters.NewIntParameterWithDefault("limit", 20, "Optional: The maximum number of stored procedures to return. Defaults to 20."),
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
if cfg.Description == "" {
|
||||
cfg.Description = "Retrieves stored procedure metadata returning schema name, procedure name, procedure owner, language, definition, and description, filtered by optional role name (procedure owner), schema name, and limit (default 20)."
|
||||
}
|
||||
|
||||
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
name: cfg.Name,
|
||||
kind: cfg.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 {
|
||||
Config
|
||||
name string `yaml:"name"`
|
||||
kind string `yaml:"kind"`
|
||||
authRequired []string `yaml:"authRequired"`
|
||||
allParams parameters.Parameters `yaml:"allParams"`
|
||||
pool *pgxpool.Pool
|
||||
manifest tools.Manifest
|
||||
mcpManifest tools.McpManifest
|
||||
}
|
||||
|
||||
func (t Tool) ToConfig() tools.ToolConfig {
|
||||
return t.Config
|
||||
}
|
||||
|
||||
func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, error) {
|
||||
paramsMap := params.AsMap()
|
||||
|
||||
newParams, err := parameters.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, listStoredProcedure, 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) (parameters.ParamValues, error) {
|
||||
return parameters.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(resourceMgr tools.SourceProvider) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) {
|
||||
return "Authorization", nil
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright 2026 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 postgresliststoredprocedure_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/postgresliststoredprocedure"
|
||||
)
|
||||
|
||||
func TestParseFromYamlPostgresListStoredProcedure(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-stored-procedure
|
||||
source: my-postgres-instance
|
||||
description: some description
|
||||
authRequired:
|
||||
- my-google-auth-service
|
||||
- other-auth-service
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"example_tool": postgresliststoredprocedure.Config{
|
||||
Name: "example_tool",
|
||||
Kind: "postgres-list-stored-procedure",
|
||||
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-stored-procedure
|
||||
source: my-postgres-instance
|
||||
description: some description
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"example_tool": postgresliststoredprocedure.Config{
|
||||
Name: "example_tool",
|
||||
Kind: "postgres-list-stored-procedure",
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -201,6 +201,7 @@ func TestAlloyDBPgToolEndpoints(t *testing.T) {
|
||||
tests.RunPostgresListPgSettingsTest(t, ctx, pool)
|
||||
tests.RunPostgresListDatabaseStatsTest(t, ctx, pool)
|
||||
tests.RunPostgresListRolesTest(t, ctx, pool)
|
||||
tests.RunPostgresListStoredProcedureTest(t, ctx, pool)
|
||||
}
|
||||
|
||||
// Test connection with different IP type
|
||||
|
||||
@@ -185,6 +185,7 @@ func TestCloudSQLPgSimpleToolEndpoints(t *testing.T) {
|
||||
tests.RunPostgresListPgSettingsTest(t, ctx, pool)
|
||||
tests.RunPostgresListDatabaseStatsTest(t, ctx, pool)
|
||||
tests.RunPostgresListRolesTest(t, ctx, pool)
|
||||
tests.RunPostgresListStoredProcedureTest(t, ctx, pool)
|
||||
}
|
||||
|
||||
// Test connection with different IP type
|
||||
|
||||
@@ -213,6 +213,7 @@ func AddPostgresPrebuiltConfig(t *testing.T, config map[string]any) map[string]a
|
||||
PostgresListPGSettingsToolKind = "postgres-list-pg-settings"
|
||||
PostgresListDatabaseStatsToolKind = "postgres-list-database-stats"
|
||||
PostgresListRolesToolKind = "postgres-list-roles"
|
||||
PostgresListStoredProcedureToolKind = "postgres-list-stored-procedure"
|
||||
)
|
||||
|
||||
tools, ok := config["tools"].(map[string]any)
|
||||
@@ -310,6 +311,11 @@ func AddPostgresPrebuiltConfig(t *testing.T, config map[string]any) map[string]a
|
||||
"kind": PostgresListRolesToolKind,
|
||||
"source": "my-instance",
|
||||
}
|
||||
|
||||
tools["list_stored_procedure"] = map[string]any{
|
||||
"kind": PostgresListStoredProcedureToolKind,
|
||||
"source": "my-instance",
|
||||
}
|
||||
config["tools"] = tools
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -164,4 +164,5 @@ func TestPostgres(t *testing.T) {
|
||||
tests.RunPostgresListPgSettingsTest(t, ctx, pool)
|
||||
tests.RunPostgresListDatabaseStatsTest(t, ctx, pool)
|
||||
tests.RunPostgresListRolesTest(t, ctx, pool)
|
||||
tests.RunPostgresListStoredProcedureTest(t, ctx, pool)
|
||||
}
|
||||
|
||||
251
tests/tool.go
251
tests/tool.go
@@ -4390,6 +4390,257 @@ func RunPostgresListTableStatsTest(t *testing.T, ctx context.Context, pool *pgxp
|
||||
}
|
||||
}
|
||||
|
||||
// RunPostgresListStoredProcedureTest runs tests for the postgres list-stored-procedure tool
|
||||
func RunPostgresListStoredProcedureTest(t *testing.T, ctx context.Context, pool *pgxpool.Pool) {
|
||||
type storedProcedureDetails struct {
|
||||
SchemaName string `json:"schema_name"`
|
||||
Name string `json:"name"`
|
||||
Owner string `json:"owner"`
|
||||
Language string `json:"language"`
|
||||
Definition string `json:"definition"`
|
||||
Description any `json:"description"`
|
||||
}
|
||||
|
||||
// Create test schema
|
||||
testSchemaName := "test_proc_schema_" + strings.ReplaceAll(uuid.New().String(), "-", "")
|
||||
createSchemaStmt := fmt.Sprintf("CREATE SCHEMA %s", testSchemaName)
|
||||
if _, err := pool.Exec(ctx, createSchemaStmt); err != nil {
|
||||
t.Fatalf("unable to create test schema: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
dropSchemaStmt := fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", testSchemaName)
|
||||
if _, err := pool.Exec(ctx, dropSchemaStmt); err != nil {
|
||||
t.Logf("warning: unable to drop test schema: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Create test procedures
|
||||
proc1Name := "test_proc_1_" + strings.ReplaceAll(uuid.New().String(), "-", "")
|
||||
createProc1Stmt := fmt.Sprintf(`
|
||||
CREATE PROCEDURE %s.%s(p_count INT)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO test_table VALUES (p_count);
|
||||
COMMIT;
|
||||
END;
|
||||
$$
|
||||
`, testSchemaName, proc1Name)
|
||||
|
||||
if _, err := pool.Exec(ctx, createProc1Stmt); err != nil {
|
||||
t.Fatalf("unable to create test procedure 1: %v", err)
|
||||
}
|
||||
|
||||
// Add a comment/description to the procedure
|
||||
commentStmt := fmt.Sprintf("COMMENT ON PROCEDURE %s.%s(INT) IS 'Test procedure that inserts a record'", testSchemaName, proc1Name)
|
||||
if _, err := pool.Exec(ctx, commentStmt); err != nil {
|
||||
t.Logf("warning: unable to add comment to procedure: %v", err)
|
||||
}
|
||||
|
||||
// Create a second test procedure
|
||||
proc2Name := "test_proc_2_" + strings.ReplaceAll(uuid.New().String(), "-", "")
|
||||
createProc2Stmt := fmt.Sprintf(`
|
||||
CREATE PROCEDURE %s.%s()
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_count INT;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO v_count FROM test_table;
|
||||
RAISE NOTICE 'Total records: %%', v_count;
|
||||
END;
|
||||
$$
|
||||
`, testSchemaName, proc2Name)
|
||||
|
||||
if _, err := pool.Exec(ctx, createProc2Stmt); err != nil {
|
||||
t.Fatalf("unable to create test procedure 2: %v", err)
|
||||
}
|
||||
|
||||
invokeTcs := []struct {
|
||||
name string
|
||||
requestBody io.Reader
|
||||
wantStatusCode int
|
||||
shouldHaveData bool
|
||||
expectedCount int
|
||||
filterByRole string
|
||||
filterBySchema string
|
||||
}{
|
||||
{
|
||||
name: "list stored procedures with no arguments (default limit 20)",
|
||||
requestBody: bytes.NewBufferString(`{}`),
|
||||
wantStatusCode: http.StatusOK,
|
||||
shouldHaveData: false, // may or may not have data depending on what's in the database
|
||||
},
|
||||
{
|
||||
name: "list stored procedures filtering by specific schema",
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf(`{"schema_name": "%s"}`, testSchemaName)),
|
||||
wantStatusCode: http.StatusOK,
|
||||
shouldHaveData: true,
|
||||
expectedCount: 2,
|
||||
filterBySchema: testSchemaName,
|
||||
},
|
||||
{
|
||||
name: "list stored procedures filtering by procedure owner (postgres)",
|
||||
requestBody: bytes.NewBufferString(`{"role_name": "postgres"}`),
|
||||
wantStatusCode: http.StatusOK,
|
||||
shouldHaveData: false, // might have procedures owned by postgres
|
||||
},
|
||||
{
|
||||
name: "list stored procedures with custom limit",
|
||||
requestBody: bytes.NewBufferString(`{"limit": 5}`),
|
||||
wantStatusCode: http.StatusOK,
|
||||
shouldHaveData: false,
|
||||
},
|
||||
{
|
||||
name: "list stored procedures filtering by schema and role",
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf(`{"schema_name": "%s", "role_name": "postgres"}`, testSchemaName)),
|
||||
wantStatusCode: http.StatusOK,
|
||||
shouldHaveData: true,
|
||||
expectedCount: 2,
|
||||
filterBySchema: testSchemaName,
|
||||
filterByRole: "postgres",
|
||||
},
|
||||
{
|
||||
name: "list stored procedures with non-existent schema",
|
||||
requestBody: bytes.NewBufferString(`{"schema_name": "non_existent_schema_xyz"}`),
|
||||
wantStatusCode: http.StatusOK,
|
||||
shouldHaveData: false,
|
||||
},
|
||||
{
|
||||
name: "list stored procedures with non-existent role",
|
||||
requestBody: bytes.NewBufferString(`{"role_name": "non_existent_role_xyz"}`),
|
||||
wantStatusCode: http.StatusOK,
|
||||
shouldHaveData: false,
|
||||
},
|
||||
{
|
||||
name: "list stored procedures with partial schema name match",
|
||||
requestBody: bytes.NewBufferString(`{"schema_name": "test_proc"}`),
|
||||
wantStatusCode: http.StatusOK,
|
||||
shouldHaveData: true,
|
||||
expectedCount: 2,
|
||||
},
|
||||
{
|
||||
name: "list stored procedures with limit 1",
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf(`{"schema_name": "%s", "limit": 1}`, testSchemaName)),
|
||||
wantStatusCode: http.StatusOK,
|
||||
shouldHaveData: true,
|
||||
expectedCount: 1,
|
||||
filterBySchema: testSchemaName,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range invokeTcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
const api = "http://127.0.0.1:5000/api/tool/list_stored_procedure/invoke"
|
||||
resp, respBody := RunRequest(t, http.MethodPost, api, tc.requestBody, nil)
|
||||
if resp.StatusCode != tc.wantStatusCode {
|
||||
t.Fatalf("wrong status code: got %d, want %d, body: %s", resp.StatusCode, tc.wantStatusCode, string(respBody))
|
||||
}
|
||||
if tc.wantStatusCode != http.StatusOK {
|
||||
return
|
||||
}
|
||||
|
||||
var bodyWrapper struct {
|
||||
Result json.RawMessage `json:"result"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &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 []storedProcedureDetails
|
||||
if resultString != "null" {
|
||||
if err := json.Unmarshal([]byte(resultString), &got); err != nil {
|
||||
t.Fatalf("failed to unmarshal result: %v, result string: %s", err, resultString)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify expected data presence
|
||||
if tc.shouldHaveData {
|
||||
if len(got) == 0 {
|
||||
t.Fatalf("expected data but got empty result")
|
||||
}
|
||||
|
||||
// If filtering by schema, verify all results are from that schema
|
||||
if tc.filterBySchema != "" {
|
||||
for _, proc := range got {
|
||||
if proc.SchemaName != tc.filterBySchema && !strings.Contains(proc.SchemaName, tc.filterBySchema) {
|
||||
t.Errorf("procedure schema %s does not match filter %s", proc.SchemaName, tc.filterBySchema)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If filtering by role, verify all results are owned by that role
|
||||
if tc.filterByRole != "" {
|
||||
for _, proc := range got {
|
||||
if proc.Owner != tc.filterByRole {
|
||||
t.Errorf("procedure owner %s does not match filter %s", proc.Owner, tc.filterByRole)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify expected count if specified
|
||||
if tc.expectedCount > 0 && len(got) != tc.expectedCount {
|
||||
t.Errorf("expected %d procedures but got %d", tc.expectedCount, len(got))
|
||||
}
|
||||
}
|
||||
|
||||
// Verify result structure and data types
|
||||
for _, proc := range got {
|
||||
// Verify all required fields are present and non-empty
|
||||
if proc.SchemaName == "" {
|
||||
t.Errorf("schema_name should not be empty")
|
||||
}
|
||||
if proc.Name == "" {
|
||||
t.Errorf("procedure name should not be empty")
|
||||
}
|
||||
if proc.Owner == "" {
|
||||
t.Errorf("owner should not be empty")
|
||||
}
|
||||
if proc.Language == "" {
|
||||
t.Errorf("language should not be empty")
|
||||
}
|
||||
if proc.Definition == "" {
|
||||
t.Errorf("definition should not be empty")
|
||||
}
|
||||
|
||||
// Verify definition contains CREATE PROCEDURE
|
||||
if !strings.Contains(proc.Definition, "CREATE PROCEDURE") {
|
||||
t.Logf("warning: definition may not be a valid CREATE PROCEDURE statement: %s", proc.Definition)
|
||||
}
|
||||
|
||||
// Verify language is a valid PostgreSQL language
|
||||
validLanguages := []string{"plpgsql", "sql", "c", "internal", "plperl", "pltcl", "plpython"}
|
||||
found := false
|
||||
for _, lang := range validLanguages {
|
||||
if proc.Language == lang {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Logf("warning: language %s may not be a standard PostgreSQL language", proc.Language)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify results are sorted by schema_name and name
|
||||
if len(got) > 1 {
|
||||
for i := 0; i < len(got)-1; i++ {
|
||||
currentKey := fmt.Sprintf("%s.%s", got[i].SchemaName, got[i].Name)
|
||||
nextKey := fmt.Sprintf("%s.%s", got[i+1].SchemaName, got[i+1].Name)
|
||||
if currentKey > nextKey {
|
||||
t.Logf("warning: results may not be sorted by schema_name and name")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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