mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-11 00:18:17 -05:00
feat(firebird): Add Firebird SQL 2.5+ source and tool support (#1011)
## Description This Pull Request introduces support for the Firebird database as a new `source` and adds the `firebird-sql` and `firebird-execute-sql` tools. The implementation follows the architectural pattern established by the existing PostgreSQL and MySQL tools. Comprehensive integration tests have been added in the `tests/firebird` directory, covering table creation, data insertion, and the invocation of all related tools. Corresponding user and developer documentation has also been included. ## Test Results The core functionality is working as demonstrated by the vast majority of tests passing. However, a few tests fail due to limitations and inflexibilities in the generic test harness (`tests/tool.go`), which is not fully compatible with Firebird's specific syntax and behavior. ### Known and Justified Failures: * **`invoke_my-tool` / `invoke_my-array-tool`:** The `RunToolInvokeTest` function reuses the same `want` variable for both sub-tests, which have different inputs and expected outputs. The test is configured for `my-tool` to pass, which consequently causes the `my-array-tool` test to fail its assertion. The `got` log for the array test confirms that the tool returns the correct response for the given input. * **`invoke_my-exec-sql-tool`:** This test fails because the harness sends the query `SELECT 1`, which is invalid syntax in Firebird. Subsequent tests for this tool using DDL and DML (`create`, `select`, `drop`) all pass, confirming its core functionality. * **`invoke_select-fields-templateParams-tool`:** This test fails due to a case-sensitivity mismatch in column names (`NAME` vs. `name`). The expected output in the test harness is hardcoded with a lowercase name. * **Authentication Tests:** All authentication-related tests are failing. This appears to be a general issue with the local test environment setup and is not specific to the Firebird implementation. ## Next Steps I believe the implementation is ready for review. I am available to make any requested changes. Feat #935 --------- Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com> Co-authored-by: Yuan Teoh <yuanteoh@google.com>
This commit is contained in:
@@ -553,6 +553,28 @@ steps:
|
||||
tidb \
|
||||
tidbsql tidbexecutesql
|
||||
|
||||
- id: "firebird"
|
||||
name: golang:1
|
||||
waitFor: ["compile-test-binary"]
|
||||
entrypoint: /bin/bash
|
||||
env:
|
||||
- "GOPATH=/gopath"
|
||||
- "FIREBIRD_DATABASE=$_FIREBIRD_DATABASE_NAME"
|
||||
- "FIREBIRD_HOST=$_FIREBIRD_HOST"
|
||||
- "FIREBIRD_PORT=$_FIREBIRD_PORT"
|
||||
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
|
||||
secretEnv: ["CLIENT_ID", "FIREBIRD_USER", "FIREBIRD_PASS"]
|
||||
volumes:
|
||||
- name: "go"
|
||||
path: "/gopath"
|
||||
args:
|
||||
- -c
|
||||
- |
|
||||
.ci/test_with_coverage.sh \
|
||||
"Firebird" \
|
||||
firebird \
|
||||
firebirdsql firebirdexecutesql
|
||||
|
||||
- id: "trino"
|
||||
name: golang:1
|
||||
waitFor: ["compile-test-binary"]
|
||||
@@ -638,6 +660,10 @@ availableSecrets:
|
||||
env: TIDB_USER
|
||||
- versionName: projects/$PROJECT_ID/secrets/tidb_pass/versions/latest
|
||||
env: TIDB_PASS
|
||||
- versionName: projects/$PROJECT_ID/secrets/firebird_user/versions/latest
|
||||
env: FIREBIRD_USER
|
||||
- versionName: projects/$PROJECT_ID/secrets/firebird_pass/versions/latest
|
||||
env: FIREBIRD_PASS
|
||||
- versionName: projects/$PROJECT_ID/secrets/trino_user/versions/latest
|
||||
env: TRINO_USER
|
||||
- versionName: projects/$PROJECT_ID/secrets/oceanbase_host/versions/latest
|
||||
@@ -657,6 +683,7 @@ options:
|
||||
|
||||
substitutions:
|
||||
_DATABASE_NAME: test_database
|
||||
_FIREBIRD_DATABASE_NAME: /firebird/test_database.fdb
|
||||
_REGION: "us-central1"
|
||||
_CLOUD_SQL_POSTGRES_INSTANCE: "cloud-sql-pg-testing"
|
||||
_ALLOYDB_POSTGRES_CLUSTER: "alloydb-pg-testing"
|
||||
@@ -680,6 +707,8 @@ substitutions:
|
||||
_LOOKER_VERIFY_SSL: "true"
|
||||
_TIDB_HOST: 127.0.0.1
|
||||
_TIDB_PORT: "4000"
|
||||
_FIREBIRD_HOST: 127.0.0.1
|
||||
_FIREBIRD_PORT: "3050"
|
||||
_TRINO_HOST: 127.0.0.1
|
||||
_TRINO_PORT: "8080"
|
||||
_TRINO_CATALOG: "memory"
|
||||
|
||||
@@ -56,6 +56,8 @@ import (
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/dataplex/dataplexsearchaspecttypes"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/dataplex/dataplexsearchentries"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/dgraph"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/firebird/firebirdexecutesql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/firebird/firebirdsql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestoreadddocuments"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestoredeletedocuments"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestoregetdocuments"
|
||||
@@ -123,6 +125,7 @@ import (
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/couchbase"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/dataplex"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/dgraph"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/firebird"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/firestore"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/http"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/looker"
|
||||
|
||||
59
docs/en/resources/sources/firebird.md
Normal file
59
docs/en/resources/sources/firebird.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: "Firebird"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
Firebird is a powerful, cross-platform, and open-source relational database.
|
||||
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
[Firebird][fb-docs] is a relational database management system offering many ANSI SQL standard features that runs on Linux, Windows, and a variety of Unix platforms. It is known for its small footprint, powerful features, and easy maintenance.
|
||||
|
||||
[fb-docs]: https://firebirdsql.org/
|
||||
|
||||
## Available Tools
|
||||
|
||||
- [`firebird-sql`](../tools/firebird/firebird-sql.md)
|
||||
Execute SQL queries as prepared statements in Firebird.
|
||||
|
||||
- [`firebird-execute-sql`](../tools/firebird/firebird-execute-sql.md)
|
||||
Run parameterized SQL statements in Firebird.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Database User
|
||||
|
||||
This source uses standard authentication. You will need to [create a Firebird user][fb-users] to login to the database with.
|
||||
|
||||
[fb-users]: https://firebirdsql.org/refdocs/langrefupd25-sql-create-user.html
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
sources:
|
||||
my_firebird_db:
|
||||
kind: firebird
|
||||
host: "localhost"
|
||||
port: 3050
|
||||
database: "/path/to/your/database.fdb"
|
||||
user: ${FIREBIRD_USER}
|
||||
password: ${FIREBIRD_PASS}
|
||||
```
|
||||
|
||||
{{< notice tip >}}
|
||||
Use environment variable replacement with the format ${ENV_NAME}
|
||||
instead of hardcoding your secrets into the configuration file.
|
||||
{{< /notice >}}
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|-----------|:--------:|:------------:|------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be "firebird". |
|
||||
| host | string | true | IP address to connect to (e.g. "127.0.0.1") |
|
||||
| port | string | true | Port to connect to (e.g. "3050") |
|
||||
| database | string | true | Path to the Firebird database file (e.g. "/var/lib/firebird/data/test.fdb"). |
|
||||
| user | string | true | Name of the Firebird user to connect as (e.g. "SYSDBA"). |
|
||||
| password | string | true | Password of the Firebird user (e.g. "masterkey"). |
|
||||
7
docs/en/resources/tools/firebird/_index.md
Normal file
7
docs/en/resources/tools/firebird/_index.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: "Firebird"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
Tools that work with Firebird Sources.
|
||||
---
|
||||
41
docs/en/resources/tools/firebird/firebird-execute-sql.md
Normal file
41
docs/en/resources/tools/firebird/firebird-execute-sql.md
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: "firebird-execute-sql"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
A "firebird-execute-sql" tool executes a SQL statement against a Firebird
|
||||
database.
|
||||
aliases:
|
||||
- /resources/tools/firebird-execute-sql
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
A `firebird-execute-sql` tool executes a SQL statement against a Firebird
|
||||
database. It's compatible with the following source:
|
||||
|
||||
- [firebird](../sources/firebird.md)
|
||||
|
||||
`firebird-execute-sql` takes one input parameter `sql` and runs the sql
|
||||
statement against the `source`.
|
||||
|
||||
> **Note:** This tool is intended for developer assistant workflows with
|
||||
> human-in-the-loop and shouldn't be used for production agents.
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
execute_sql_tool:
|
||||
kind: firebird-execute-sql
|
||||
source: my_firebird_db
|
||||
description: Use this tool to execute a SQL statement against the Firebird database.
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be "firebird-execute-sql". |
|
||||
| source | string | true | Name of the source the SQL should execute on. |
|
||||
| description | string | true | Description of the tool that is passed to the LLM. |
|
||||
135
docs/en/resources/tools/firebird/firebird-sql.md
Normal file
135
docs/en/resources/tools/firebird/firebird-sql.md
Normal file
@@ -0,0 +1,135 @@
|
||||
---
|
||||
title: "firebird-sql"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
A "firebird-sql" tool executes a pre-defined SQL statement against a Firebird
|
||||
database.
|
||||
aliases:
|
||||
- /resources/tools/firebird-sql
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
A `firebird-sql` tool executes a pre-defined SQL statement against a Firebird
|
||||
database. It's compatible with the following source:
|
||||
|
||||
- [firebird](../sources/firebird.md)
|
||||
|
||||
The specified SQL statement is executed as a [prepared statement][fb-prepare],
|
||||
and supports both positional parameters (`?`) and named parameters (`:param_name`).
|
||||
Parameters will be inserted according to their position or name. If template
|
||||
parameters are included, they will be resolved before the execution of the
|
||||
prepared statement.
|
||||
|
||||
[fb-prepare]: https://firebirdsql.org/refdocs/langrefupd25-psql-execstat.html
|
||||
|
||||
## Example
|
||||
|
||||
> **Note:** This tool uses parameterized queries to prevent SQL injections.
|
||||
> Query parameters can be used as substitutes for arbitrary expressions.
|
||||
> Parameters cannot be used as substitutes for identifiers, column names, table
|
||||
> names, or other parts of the query.
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
search_flights_by_number:
|
||||
kind: firebird-sql
|
||||
source: my_firebird_db
|
||||
statement: |
|
||||
SELECT * FROM flights
|
||||
WHERE airline = ?
|
||||
AND flight_number = ?
|
||||
LIMIT 10
|
||||
description: |
|
||||
Use this tool to get information for a specific flight.
|
||||
Takes an airline code and flight number and returns info on the flight.
|
||||
Do NOT use this tool with a flight id. Do NOT guess an airline code or flight number.
|
||||
A airline code is a code for an airline service consisting of two-character
|
||||
airline designator and followed by flight number, which is 1 to 4 digit number.
|
||||
For example, if given CY 0123, the airline is "CY", and flight_number is "123".
|
||||
Another example for this is DL 1234, the airline is "DL", and flight_number is "1234".
|
||||
If the tool returns more than one option choose the date closes to today.
|
||||
Example:
|
||||
{{
|
||||
"airline": "CY",
|
||||
"flight_number": "888",
|
||||
}}
|
||||
Example:
|
||||
{{
|
||||
"airline": "DL",
|
||||
"flight_number": "1234",
|
||||
}}
|
||||
parameters:
|
||||
- name: airline
|
||||
type: string
|
||||
description: Airline unique 2 letter identifier
|
||||
- name: flight_number
|
||||
type: string
|
||||
description: 1 to 4 digit number
|
||||
```
|
||||
|
||||
### Example with Named Parameters
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
search_flights_by_airline:
|
||||
kind: firebird-sql
|
||||
source: my_firebird_db
|
||||
statement: |
|
||||
SELECT * FROM flights
|
||||
WHERE airline = :airline
|
||||
AND departure_date >= :start_date
|
||||
AND departure_date <= :end_date
|
||||
ORDER BY departure_date
|
||||
description: |
|
||||
Search for flights by airline within a date range using named parameters.
|
||||
parameters:
|
||||
- name: airline
|
||||
type: string
|
||||
description: Airline unique 2 letter identifier
|
||||
- name: start_date
|
||||
type: string
|
||||
description: Start date in YYYY-MM-DD format
|
||||
- name: end_date
|
||||
type: string
|
||||
description: End date in YYYY-MM-DD format
|
||||
```
|
||||
|
||||
### Example with Template Parameters
|
||||
|
||||
> **Note:** This tool allows direct modifications to the SQL statement,
|
||||
> including identifiers, column names, and table names. **This makes it more
|
||||
> vulnerable to SQL injections**. Using basic parameters only (see above) is
|
||||
> recommended for performance and safety reasons. For more details, please check
|
||||
> [templateParameters](_index#template-parameters).
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
list_table:
|
||||
kind: firebird-sql
|
||||
source: my_firebird_db
|
||||
statement: |
|
||||
SELECT * FROM {{.tableName}}
|
||||
description: |
|
||||
Use this tool to list all information from a specific table.
|
||||
Example:
|
||||
{{
|
||||
"tableName": "flights",
|
||||
}}
|
||||
templateParameters:
|
||||
- name: tableName
|
||||
type: string
|
||||
description: Table to select from
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|---------------------|:---------------------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be "firebird-sql". |
|
||||
| source | string | true | Name of the source the SQL should execute on. |
|
||||
| description | string | true | Description of the tool that is passed to the LLM. |
|
||||
| statement | string | true | SQL statement to execute on. |
|
||||
| parameters | [parameters](_index#specifying-parameters) | false | List of [parameters](_index#specifying-parameters) that will be inserted into the SQL statement. |
|
||||
| templateParameters | [templateParameters](_index#template-parameters) | false | List of [templateParameters](_index#template-parameters) that will be inserted into the SQL statement before executing prepared statement. |
|
||||
5
go.mod
5
go.mod
@@ -31,6 +31,7 @@ require (
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/looker-open-source/sdk-codegen/go v0.25.10
|
||||
github.com/microsoft/go-mssqldb v1.9.2
|
||||
github.com/nakagami/firebirdsql v0.9.15
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.28.2
|
||||
github.com/redis/go-redis/v9 v9.12.1
|
||||
github.com/spf13/cobra v1.9.1
|
||||
@@ -117,6 +118,7 @@ require (
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
@@ -124,11 +126,13 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||
github.com/nakagami/chacha20 v0.1.0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
@@ -137,6 +141,7 @@ require (
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/zeebo/errs v1.4.0 // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
|
||||
|
||||
10
go.sum
10
go.sum
@@ -1082,6 +1082,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
@@ -1134,6 +1136,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/nakagami/chacha20 v0.1.0 h1:2fbf5KeVUw7oRpAe6/A7DqvBJLYYu0ka5WstFbnkEVo=
|
||||
github.com/nakagami/chacha20 v0.1.0/go.mod h1:xpoujepNFA7MvYLvX5xKHzlOHimDrLI9Ll8zfOJ0l2E=
|
||||
github.com/nakagami/firebirdsql v0.9.15 h1:Mf05jaFI8+kjy6sBstsAu76zOkJ44AGd6cpApWNrp/0=
|
||||
github.com/nakagami/firebirdsql v0.9.15/go.mod h1:bZKRs3rpHAjJgXAoc9YiPobTz3R22i41Zjo+llIS2B0=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.28.2 h1:uG7nMK0zS/a/iSWMZgCIY40SfYzWBc6uSrMONhiIS0U=
|
||||
@@ -1187,6 +1193,8 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
@@ -1251,6 +1259,8 @@ github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
|
||||
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs=
|
||||
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
|
||||
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
|
||||
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
|
||||
114
internal/sources/firebird/firebird.go
Normal file
114
internal/sources/firebird/firebird.go
Normal file
@@ -0,0 +1,114 @@
|
||||
// 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 firebird
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
const SourceKind string = "firebird"
|
||||
|
||||
var _ sources.SourceConfig = Config{}
|
||||
|
||||
func init() {
|
||||
if !sources.Register(SourceKind, newConfig) {
|
||||
panic(fmt.Sprintf("source kind %q already registered", SourceKind))
|
||||
}
|
||||
}
|
||||
|
||||
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources.SourceConfig, error) {
|
||||
actual := Config{Name: name}
|
||||
if err := decoder.DecodeContext(ctx, &actual); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return actual, nil
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Host string `yaml:"host" validate:"required"`
|
||||
Port string `yaml:"port" validate:"required"`
|
||||
User string `yaml:"user" validate:"required"`
|
||||
Password string `yaml:"password" validate:"required"`
|
||||
Database string `yaml:"database" validate:"required"`
|
||||
}
|
||||
|
||||
func (r Config) SourceConfigKind() string {
|
||||
return SourceKind
|
||||
}
|
||||
|
||||
func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) {
|
||||
pool, err := initFirebirdConnectionPool(ctx, tracer, r.Name, r.Host, r.Port, r.User, r.Password, r.Database)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create pool: %w", err)
|
||||
}
|
||||
|
||||
err = pool.PingContext(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to connect successfully: %w", err)
|
||||
}
|
||||
|
||||
s := &Source{
|
||||
Name: r.Name,
|
||||
Kind: SourceKind,
|
||||
Db: pool,
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
var _ sources.Source = &Source{}
|
||||
|
||||
type Source struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
Db *sql.DB
|
||||
}
|
||||
|
||||
func (s *Source) SourceKind() string {
|
||||
return SourceKind
|
||||
}
|
||||
|
||||
func (s *Source) FirebirdDB() *sql.DB {
|
||||
return s.Db
|
||||
}
|
||||
|
||||
func initFirebirdConnectionPool(ctx context.Context, tracer trace.Tracer, name, host, port, user, pass, dbname string) (*sql.DB, error) {
|
||||
_, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, name)
|
||||
defer span.End()
|
||||
|
||||
// urlExample := "user:password@host:port/path/to/database.fdb"
|
||||
dsn := fmt.Sprintf("%s:%s@%s:%s/%s", user, pass, host, port, dbname)
|
||||
|
||||
db, err := sql.Open("firebirdsql", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create connection pool: %w", err)
|
||||
}
|
||||
|
||||
// Configure connection pool to prevent deadlocks
|
||||
db.SetMaxOpenConns(5)
|
||||
db.SetMaxIdleConns(2)
|
||||
db.SetConnMaxLifetime(5 * time.Minute)
|
||||
db.SetConnMaxIdleTime(1 * time.Minute)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
127
internal/sources/firebird/firebird_test.go
Normal file
127
internal/sources/firebird/firebird_test.go
Normal file
@@ -0,0 +1,127 @@
|
||||
// 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 firebird_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/sources/firebird"
|
||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||
)
|
||||
|
||||
func TestParseFromYamlFirebird(t *testing.T) {
|
||||
tcs := []struct {
|
||||
desc string
|
||||
in string
|
||||
want server.SourceConfigs
|
||||
}{
|
||||
{
|
||||
desc: "basic example",
|
||||
in: `
|
||||
sources:
|
||||
my-fdb-instance:
|
||||
kind: firebird
|
||||
host: my-host
|
||||
port: my-port
|
||||
database: my_db
|
||||
user: my_user
|
||||
password: my_pass
|
||||
`,
|
||||
want: server.SourceConfigs{
|
||||
"my-fdb-instance": firebird.Config{
|
||||
Name: "my-fdb-instance",
|
||||
Kind: firebird.SourceKind,
|
||||
Host: "my-host",
|
||||
Port: "my-port",
|
||||
Database: "my_db",
|
||||
User: "my_user",
|
||||
Password: "my_pass",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := struct {
|
||||
Sources server.SourceConfigs `yaml:"sources"`
|
||||
}{}
|
||||
// Parse contents
|
||||
err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to unmarshal: %s", err)
|
||||
}
|
||||
if !cmp.Equal(tc.want, got.Sources) {
|
||||
t.Fatalf("incorrect parse: want %v, got %v", tc.want, got.Sources)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFailParseFromYaml(t *testing.T) {
|
||||
tcs := []struct {
|
||||
desc string
|
||||
in string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
desc: "extra field",
|
||||
in: `
|
||||
sources:
|
||||
my-fdb-instance:
|
||||
kind: firebird
|
||||
host: my-host
|
||||
port: my-port
|
||||
database: my_db
|
||||
user: my_user
|
||||
password: my_pass
|
||||
foo: bar
|
||||
`,
|
||||
err: "unable to parse source \"my-fdb-instance\" as \"firebird\": [2:1] unknown field \"foo\"\n 1 | database: my_db\n> 2 | foo: bar\n ^\n 3 | host: my-host\n 4 | kind: firebird\n 5 | password: my_pass\n 6 | ",
|
||||
},
|
||||
{
|
||||
desc: "missing required field",
|
||||
in: `
|
||||
sources:
|
||||
my-fdb-instance:
|
||||
kind: firebird
|
||||
host: my-host
|
||||
port: my-port
|
||||
database: my_db
|
||||
user: my_user
|
||||
`,
|
||||
err: "unable to parse source \"my-fdb-instance\" as \"firebird\": Key: 'Config.Password' Error:Field validation for 'Password' failed on the 'required' tag",
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := struct {
|
||||
Sources server.SourceConfigs `yaml:"sources"`
|
||||
}{}
|
||||
// Parse contents
|
||||
err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got)
|
||||
if err == nil {
|
||||
t.Fatalf("expect parsing to fail")
|
||||
}
|
||||
errStr := err.Error()
|
||||
if errStr != tc.err {
|
||||
t.Fatalf("unexpected error: got %q, want %q", errStr, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
188
internal/tools/firebird/firebirdexecutesql/firebirdexecutesql.go
Normal file
188
internal/tools/firebird/firebirdexecutesql/firebirdexecutesql.go
Normal file
@@ -0,0 +1,188 @@
|
||||
// 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 firebirdexecutesql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources/firebird"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
)
|
||||
|
||||
const kind string = "firebird-execute-sql"
|
||||
|
||||
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 {
|
||||
FirebirdDB() *sql.DB
|
||||
}
|
||||
|
||||
var _ compatibleSource = &firebird.Source{}
|
||||
|
||||
var compatibleSources = [...]string{firebird.SourceKind}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
}
|
||||
|
||||
var _ tools.ToolConfig = Config{}
|
||||
|
||||
func (cfg Config) ToolConfigKind() string {
|
||||
return kind
|
||||
}
|
||||
|
||||
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
|
||||
rawS, ok := srcs[cfg.Source]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no source named %q configured", cfg.Source)
|
||||
}
|
||||
|
||||
s, ok := rawS.(compatibleSource)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
|
||||
}
|
||||
|
||||
sqlParameter := tools.NewStringParameter("sql", "The sql to execute.")
|
||||
parameters := tools.Parameters{sqlParameter}
|
||||
|
||||
_, paramManifest, paramMcpManifest, err := tools.ProcessParameters(nil, parameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
|
||||
t := &Tool{
|
||||
Name: cfg.Name,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Db: s.FirebirdDB(),
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
var _ tools.Tool = &Tool{}
|
||||
|
||||
type Tool struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
Parameters tools.Parameters `yaml:"parameters"`
|
||||
|
||||
Db *sql.DB
|
||||
manifest tools.Manifest
|
||||
mcpManifest tools.McpManifest
|
||||
}
|
||||
|
||||
func (t *Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) {
|
||||
paramsMap := params.AsMap()
|
||||
sql, ok := paramsMap["sql"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to get cast %s", paramsMap["sql"])
|
||||
}
|
||||
|
||||
// Log the query executed for debugging.
|
||||
logger, err := util.LoggerFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting logger: %s", err)
|
||||
}
|
||||
logger.DebugContext(ctx, "executing `%s` tool query: %s", kind, sql)
|
||||
|
||||
rows, err := t.Db.QueryContext(ctx, sql)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to execute query: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
cols, err := rows.Columns()
|
||||
|
||||
var out []any
|
||||
if err == nil && len(cols) > 0 {
|
||||
values := make([]any, len(cols))
|
||||
scanArgs := make([]any, len(values))
|
||||
for i := range values {
|
||||
scanArgs[i] = &values[i]
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
err = rows.Scan(scanArgs...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse row: %w", err)
|
||||
}
|
||||
|
||||
vMap := make(map[string]any)
|
||||
for i, colName := range cols {
|
||||
if b, ok := values[i].([]byte); ok {
|
||||
vMap[colName] = string(b)
|
||||
} else {
|
||||
vMap[colName] = values[i]
|
||||
}
|
||||
}
|
||||
out = append(out, vMap)
|
||||
}
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error iterating rows: %w", err)
|
||||
}
|
||||
|
||||
// In most cases, DML/DDL statements like INSERT, UPDATE, CREATE, etc. might return no rows
|
||||
// However, it is also possible that this was a query that was expected to return rows
|
||||
// but returned none, a case that we cannot distinguish here.
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (t *Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
|
||||
return tools.ParseParams(t.Parameters, 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)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firebirdexecutesql_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/firebird/firebirdexecutesql"
|
||||
)
|
||||
|
||||
func TestParseFromYamlExecuteSql(t *testing.T) {
|
||||
ctx, err := testutils.ContextWithNewLogger()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
tcs := []struct {
|
||||
desc string
|
||||
in string
|
||||
want server.ToolConfigs
|
||||
}{
|
||||
{
|
||||
desc: "basic example",
|
||||
in: `
|
||||
tools:
|
||||
example_tool:
|
||||
kind: firebird-execute-sql
|
||||
source: my-instance
|
||||
description: some description
|
||||
authRequired:
|
||||
- my-google-auth-service
|
||||
- other-auth-service
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"example_tool": firebirdexecutesql.Config{
|
||||
Name: "example_tool",
|
||||
Kind: "firebird-execute-sql",
|
||||
Source: "my-instance",
|
||||
Description: "some description",
|
||||
AuthRequired: []string{"my-google-auth-service", "other-auth-service"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := struct {
|
||||
Tools server.ToolConfigs `yaml:"tools"`
|
||||
}{}
|
||||
// Parse contents
|
||||
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to unmarshal: %s", err)
|
||||
}
|
||||
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
|
||||
t.Fatalf("incorrect parse: diff %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
214
internal/tools/firebird/firebirdsql/firebirdsql.go
Normal file
214
internal/tools/firebird/firebirdsql/firebirdsql.go
Normal file
@@ -0,0 +1,214 @@
|
||||
// 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 firebirdsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources/firebird"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
|
||||
const kind string = "firebird-sql"
|
||||
|
||||
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 {
|
||||
FirebirdDB() *sql.DB
|
||||
}
|
||||
|
||||
// validate compatible sources are still compatible
|
||||
var _ compatibleSource = &firebird.Source{}
|
||||
|
||||
var compatibleSources = [...]string{firebird.SourceKind}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
Statement string `yaml:"statement" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
Parameters tools.Parameters `yaml:"parameters"`
|
||||
TemplateParameters tools.Parameters `yaml:"templateParameters"`
|
||||
}
|
||||
|
||||
// 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, paramManifest, paramMcpManifest, err := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
|
||||
// finish tool setup
|
||||
t := &Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: cfg.Parameters,
|
||||
TemplateParameters: cfg.TemplateParameters,
|
||||
AllParams: allParameters,
|
||||
Statement: cfg.Statement,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Db: s.FirebirdDB(),
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// validate interface
|
||||
var _ tools.Tool = &Tool{}
|
||||
|
||||
type Tool struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
Parameters tools.Parameters `yaml:"parameters"`
|
||||
TemplateParameters tools.Parameters `yaml:"templateParameters"`
|
||||
AllParams tools.Parameters `yaml:"allParams"`
|
||||
|
||||
Db *sql.DB
|
||||
Statement string
|
||||
manifest tools.Manifest
|
||||
mcpManifest tools.McpManifest
|
||||
}
|
||||
|
||||
func (t *Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) {
|
||||
paramsMap := params.AsMap()
|
||||
statement, err := tools.ResolveTemplateParams(t.TemplateParameters, t.Statement, paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to extract template params: %w", err)
|
||||
}
|
||||
|
||||
newParams, err := tools.GetParams(t.Parameters, paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to extract standard params: %w", err)
|
||||
}
|
||||
|
||||
namedArgs := make([]any, 0, len(newParams))
|
||||
// To support both named args (e.g :id) and positional args (e.g ?), check
|
||||
// if arg name is contained in the statement.
|
||||
for _, p := range t.Parameters {
|
||||
name := p.GetName()
|
||||
value := paramsMap[name]
|
||||
if strings.Contains(statement, ":"+name) {
|
||||
namedArgs = append(namedArgs, sql.Named(name, value))
|
||||
} else {
|
||||
namedArgs = append(namedArgs, value)
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := t.Db.QueryContext(ctx, statement, namedArgs...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to execute query: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
cols, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get columns: %w", err)
|
||||
}
|
||||
|
||||
values := make([]any, len(cols))
|
||||
scanArgs := make([]any, len(values))
|
||||
for i := range values {
|
||||
scanArgs[i] = &values[i]
|
||||
}
|
||||
|
||||
var out []any
|
||||
for rows.Next() {
|
||||
|
||||
err = rows.Scan(scanArgs...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse row: %w", err)
|
||||
}
|
||||
|
||||
vMap := make(map[string]any)
|
||||
for i, col := range cols {
|
||||
if b, ok := values[i].([]byte); ok {
|
||||
vMap[col] = string(b)
|
||||
} else {
|
||||
vMap[col] = values[i]
|
||||
}
|
||||
}
|
||||
out = append(out, vMap)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error iterating rows: %w", err)
|
||||
}
|
||||
|
||||
// In most cases, DML/DDL statements like INSERT, UPDATE, CREATE, etc. might return no rows
|
||||
// However, it is also possible that this was a query that was expected to return rows
|
||||
// but returned none, a case that we cannot distinguish here.
|
||||
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)
|
||||
}
|
||||
167
internal/tools/firebird/firebirdsql/firebirdsql_test.go
Normal file
167
internal/tools/firebird/firebirdsql/firebirdsql_test.go
Normal file
@@ -0,0 +1,167 @@
|
||||
// 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 firebirdsql_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"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/firebird/firebirdsql"
|
||||
)
|
||||
|
||||
func TestParseFromYamlFirebird(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: firebird-sql
|
||||
source: my-fdb-instance
|
||||
description: some description
|
||||
statement: |
|
||||
SELECT * FROM SQL_STATEMENT;
|
||||
authRequired:
|
||||
- my-google-auth-service
|
||||
- other-auth-service
|
||||
parameters:
|
||||
- name: country
|
||||
type: string
|
||||
description: some description
|
||||
authServices:
|
||||
- name: my-google-auth-service
|
||||
field: user_id
|
||||
- name: other-auth-service
|
||||
field: user_id
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"example_tool": firebirdsql.Config{
|
||||
Name: "example_tool",
|
||||
Kind: "firebird-sql",
|
||||
Source: "my-fdb-instance",
|
||||
Description: "some description",
|
||||
Statement: "SELECT * FROM SQL_STATEMENT;\n",
|
||||
AuthRequired: []string{"my-google-auth-service", "other-auth-service"},
|
||||
Parameters: []tools.Parameter{
|
||||
tools.NewStringParameterWithAuth("country", "some description",
|
||||
[]tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"},
|
||||
{Name: "other-auth-service", Field: "user_id"}}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestParseFromYamlWithTemplateParamsFirebird(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: firebird-sql
|
||||
source: my-fdb-instance
|
||||
description: some description
|
||||
statement: |
|
||||
SELECT * FROM SQL_STATEMENT;
|
||||
parameters:
|
||||
- name: name
|
||||
type: string
|
||||
description: some description
|
||||
templateParameters:
|
||||
- name: tableName
|
||||
type: string
|
||||
description: The table to select hotels from.
|
||||
- name: fieldArray
|
||||
type: array
|
||||
description: The columns to return for the query.
|
||||
items:
|
||||
name: column
|
||||
type: string
|
||||
description: A column name that will be returned from the query.
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"example_tool": firebirdsql.Config{
|
||||
Name: "example_tool",
|
||||
Kind: "firebird-sql",
|
||||
Source: "my-fdb-instance",
|
||||
Description: "some description",
|
||||
Statement: "SELECT * FROM SQL_STATEMENT;\n",
|
||||
AuthRequired: []string{},
|
||||
Parameters: []tools.Parameter{
|
||||
tools.NewStringParameter("name", "some description"),
|
||||
},
|
||||
TemplateParameters: []tools.Parameter{
|
||||
tools.NewStringParameter("tableName", "The table to select hotels from."),
|
||||
tools.NewArrayParameter("fieldArray", "The columns to return for the query.", tools.NewStringParameter("column", "A column name that will be returned from the query.")),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
463
tests/firebird/firebird_integration_test.go
Normal file
463
tests/firebird/firebird_integration_test.go
Normal file
@@ -0,0 +1,463 @@
|
||||
// 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 firebird
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/tests"
|
||||
_ "github.com/nakagami/firebirdsql"
|
||||
)
|
||||
|
||||
var (
|
||||
FirebirdSourceKind = "firebird"
|
||||
FirebirdToolKind = "firebird-sql"
|
||||
FirebirdDatabase = os.Getenv("FIREBIRD_DATABASE")
|
||||
FirebirdHost = os.Getenv("FIREBIRD_HOST")
|
||||
FirebirdPort = os.Getenv("FIREBIRD_PORT")
|
||||
FirebirdUser = os.Getenv("FIREBIRD_USER")
|
||||
FirebirdPass = os.Getenv("FIREBIRD_PASS")
|
||||
)
|
||||
|
||||
func getFirebirdVars(t *testing.T) map[string]any {
|
||||
switch "" {
|
||||
case FirebirdDatabase:
|
||||
t.Fatal("'FIREBIRD_DATABASE' not set")
|
||||
case FirebirdHost:
|
||||
t.Fatal("'FIREBIRD_HOST' not set")
|
||||
case FirebirdPort:
|
||||
t.Fatal("'FIREBIRD_PORT' not set")
|
||||
case FirebirdUser:
|
||||
t.Fatal("'FIREBIRD_USER' not set")
|
||||
case FirebirdPass:
|
||||
t.Fatal("'FIREBIRD_PASS' not set")
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"kind": FirebirdSourceKind,
|
||||
"host": FirebirdHost,
|
||||
"port": FirebirdPort,
|
||||
"database": FirebirdDatabase,
|
||||
"user": FirebirdUser,
|
||||
"password": FirebirdPass,
|
||||
}
|
||||
}
|
||||
|
||||
func initFirebirdConnection(host, port, user, pass, dbname string) (*sql.DB, error) {
|
||||
dsn := fmt.Sprintf("%s:%s@%s:%s/%s", user, pass, host, port, dbname)
|
||||
db, err := sql.Open("firebirdsql", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create connection pool: %w", err)
|
||||
}
|
||||
|
||||
// Configure connection pool to prevent deadlocks
|
||||
db.SetMaxOpenConns(5)
|
||||
db.SetMaxIdleConns(2)
|
||||
db.SetConnMaxLifetime(5 * time.Minute)
|
||||
db.SetConnMaxIdleTime(1 * time.Minute)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func TestFirebirdToolEndpoints(t *testing.T) {
|
||||
sourceConfig := getFirebirdVars(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
var args []string
|
||||
|
||||
db, err := initFirebirdConnection(FirebirdHost, FirebirdPort, FirebirdUser, FirebirdPass, FirebirdDatabase)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create firebird connection pool: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
shortUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
||||
tableNameParam := fmt.Sprintf("param_table_%s", shortUUID)
|
||||
tableNameAuth := fmt.Sprintf("auth_table_%s", shortUUID)
|
||||
tableNameTemplateParam := fmt.Sprintf("template_param_table_%s", shortUUID)
|
||||
|
||||
createParamTableStmts, insertParamTableStmt, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, paramTestParams := getFirebirdParamToolInfo(tableNameParam)
|
||||
teardownTable1 := setupFirebirdTable(t, ctx, db, createParamTableStmts, insertParamTableStmt, tableNameParam, paramTestParams)
|
||||
defer teardownTable1(t)
|
||||
|
||||
createAuthTableStmts, insertAuthTableStmt, authToolStmt, authTestParams := getFirebirdAuthToolInfo(tableNameAuth)
|
||||
teardownTable2 := setupFirebirdTable(t, ctx, db, createAuthTableStmts, insertAuthTableStmt, tableNameAuth, authTestParams)
|
||||
defer teardownTable2(t)
|
||||
|
||||
toolsFile := getFirebirdToolsConfig(sourceConfig, FirebirdToolKind, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, authToolStmt)
|
||||
toolsFile = addFirebirdExecuteSqlConfig(t, toolsFile)
|
||||
tmplSelectCombined, tmplSelectFilterCombined := getFirebirdTmplToolStatement()
|
||||
toolsFile = addFirebirdTemplateParamConfig(t, toolsFile, FirebirdToolKind, tmplSelectCombined, tmplSelectFilterCombined)
|
||||
|
||||
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
|
||||
if err != nil {
|
||||
t.Fatalf("command initialization returned an error: %s", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
waitCtx, cancelWait := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancelWait()
|
||||
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
|
||||
if err != nil {
|
||||
t.Logf("toolbox command logs: \n%s", out)
|
||||
t.Fatalf("toolbox didn't start successfully: %s", err)
|
||||
}
|
||||
|
||||
// Get configs for tests
|
||||
select1Want, failInvocationWant, createTableStatement := getFirebirdWants()
|
||||
nullWant := `[{"id":4,"name":null}]`
|
||||
select1Statement := `"SELECT 1 AS \"constant\" FROM RDB$DATABASE;"`
|
||||
templateParamCreateColArray := `["id INTEGER","name VARCHAR(255)","age INTEGER"]`
|
||||
|
||||
// Run tests
|
||||
tests.RunToolGetTest(t)
|
||||
tests.RunToolInvokeTest(t, select1Want,
|
||||
tests.WithNullWant(nullWant),
|
||||
tests.DisableArrayTest())
|
||||
tests.RunMCPToolCallMethod(t, failInvocationWant)
|
||||
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want, tests.WithSelect1Statement(select1Statement))
|
||||
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam,
|
||||
tests.WithCreateColArray(templateParamCreateColArray))
|
||||
}
|
||||
|
||||
func setupFirebirdTable(t *testing.T, ctx context.Context, db *sql.DB, createStatements []string, insertStatement, tableName string, params []any) func(*testing.T) {
|
||||
err := db.PingContext(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to connect to test database: %s", err)
|
||||
}
|
||||
|
||||
for _, stmt := range createStatements {
|
||||
_, err = db.ExecContext(ctx, stmt)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to execute create statement for table %s: %s\nStatement: %s", tableName, err, stmt)
|
||||
}
|
||||
}
|
||||
|
||||
if insertStatement != "" && len(params) > 0 {
|
||||
stmt, err := db.PrepareContext(ctx, insertStatement)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to prepare insert statement: %v", err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
numPlaceholders := strings.Count(insertStatement, "?")
|
||||
if numPlaceholders == 0 {
|
||||
t.Fatalf("insert statement has no placeholders '?' but params were provided")
|
||||
}
|
||||
for i := 0; i < len(params); i += numPlaceholders {
|
||||
end := i + numPlaceholders
|
||||
if end > len(params) {
|
||||
end = len(params)
|
||||
}
|
||||
batchParams := params[i:end]
|
||||
|
||||
_, err = stmt.ExecContext(ctx, batchParams...)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to insert test data row with params %v: %v", batchParams, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return func(t *testing.T) {
|
||||
// Close the main connection to free up resources
|
||||
db.Close()
|
||||
|
||||
// Helper function to check if error indicates object doesn't exist
|
||||
isNotFoundError := func(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
errMsg := strings.ToLower(err.Error())
|
||||
return strings.Contains(errMsg, "does not exist") ||
|
||||
strings.Contains(errMsg, "not found") ||
|
||||
strings.Contains(errMsg, "is not defined") ||
|
||||
strings.Contains(errMsg, "unknown") ||
|
||||
strings.Contains(errMsg, "invalid")
|
||||
}
|
||||
|
||||
// Create dedicated cleanup connection with minimal configuration
|
||||
createCleanupConnection := func() (*sql.DB, error) {
|
||||
dsn := fmt.Sprintf("%s:%s@%s:%s/%s", FirebirdUser, FirebirdPass, FirebirdHost, FirebirdPort, FirebirdDatabase)
|
||||
|
||||
cleanupDb, err := sql.Open("firebirdsql", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ultra minimal connection pool for cleanup only
|
||||
cleanupDb.SetMaxOpenConns(1)
|
||||
cleanupDb.SetMaxIdleConns(0)
|
||||
cleanupDb.SetConnMaxLifetime(5 * time.Second)
|
||||
cleanupDb.SetConnMaxIdleTime(1 * time.Second)
|
||||
|
||||
return cleanupDb, nil
|
||||
}
|
||||
|
||||
// Drop each object with its own dedicated connection and aggressive timeout
|
||||
dropObjects := []struct {
|
||||
objType string
|
||||
query string
|
||||
}{
|
||||
{"trigger", fmt.Sprintf("DROP TRIGGER BI_%s_ID", tableName)},
|
||||
{"table", fmt.Sprintf("DROP TABLE %s", tableName)},
|
||||
{"generator", fmt.Sprintf("DROP GENERATOR GEN_%s_ID", tableName)},
|
||||
}
|
||||
|
||||
for _, obj := range dropObjects {
|
||||
cleanupDb, err := createCleanupConnection()
|
||||
if err != nil {
|
||||
t.Logf("Failed to create cleanup connection for %s: %s", obj.objType, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Use aggressive short timeout for each operation
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
_, dropErr := cleanupDb.ExecContext(ctx, obj.query)
|
||||
cancel()
|
||||
cleanupDb.Close()
|
||||
|
||||
if dropErr == nil {
|
||||
t.Logf("Successfully dropped %s", obj.objType)
|
||||
} else if isNotFoundError(dropErr) {
|
||||
t.Logf("%s does not exist, skipping", obj.objType)
|
||||
} else if ctx.Err() == context.DeadlineExceeded {
|
||||
t.Logf("Timeout dropping %s (3s limit exceeded) - continuing anyway", obj.objType)
|
||||
} else {
|
||||
t.Logf("Failed to drop %s: %s - continuing anyway", obj.objType, dropErr)
|
||||
}
|
||||
|
||||
// Small delay between operations to reduce contention
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getFirebirdParamToolInfo(tableName string) ([]string, string, string, string, string, string, []any) {
|
||||
createStatements := []string{
|
||||
fmt.Sprintf("CREATE TABLE %s (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(255));", tableName),
|
||||
fmt.Sprintf("CREATE GENERATOR GEN_%s_ID;", tableName),
|
||||
fmt.Sprintf(`
|
||||
CREATE TRIGGER BI_%s_ID FOR %s
|
||||
ACTIVE BEFORE INSERT POSITION 0
|
||||
AS
|
||||
BEGIN
|
||||
IF (NEW.id IS NULL) THEN
|
||||
NEW.id = GEN_ID(GEN_%s_ID, 1);
|
||||
END;
|
||||
`, tableName, tableName, tableName),
|
||||
}
|
||||
|
||||
insertStatement := fmt.Sprintf("INSERT INTO %s (name) VALUES (?);", tableName)
|
||||
toolStatement := fmt.Sprintf("SELECT id AS \"id\", name AS \"name\" FROM %s WHERE id = ? OR name = ?;", tableName)
|
||||
idParamStatement := fmt.Sprintf("SELECT id AS \"id\", name AS \"name\" FROM %s WHERE id = ?;", tableName)
|
||||
nameParamStatement := fmt.Sprintf("SELECT id AS \"id\", name AS \"name\" FROM %s WHERE name IS NOT DISTINCT FROM ?;", tableName)
|
||||
// Firebird doesn't support array parameters in IN clause the same way as other databases
|
||||
// We'll use a simpler approach for testing
|
||||
arrayToolStatement := fmt.Sprintf("SELECT id AS \"id\", name AS \"name\" FROM %s WHERE id = ? ORDER BY id;", tableName)
|
||||
|
||||
params := []any{"Alice", "Jane", "Sid", nil}
|
||||
return createStatements, insertStatement, toolStatement, idParamStatement, nameParamStatement, arrayToolStatement, params
|
||||
}
|
||||
|
||||
func getFirebirdAuthToolInfo(tableName string) ([]string, string, string, []any) {
|
||||
createStatements := []string{
|
||||
fmt.Sprintf("CREATE TABLE %s (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(255), email VARCHAR(255));", tableName),
|
||||
fmt.Sprintf("CREATE GENERATOR GEN_%s_ID;", tableName),
|
||||
fmt.Sprintf(`
|
||||
CREATE TRIGGER BI_%s_ID FOR %s
|
||||
ACTIVE BEFORE INSERT POSITION 0
|
||||
AS
|
||||
BEGIN
|
||||
IF (NEW.id IS NULL) THEN
|
||||
NEW.id = GEN_ID(GEN_%s_ID, 1);
|
||||
END;
|
||||
`, tableName, tableName, tableName),
|
||||
}
|
||||
|
||||
insertStatement := fmt.Sprintf("INSERT INTO %s (name, email) VALUES (?, ?)", tableName)
|
||||
toolStatement := fmt.Sprintf("SELECT name AS \"name\" FROM %s WHERE email = ?;", tableName)
|
||||
params := []any{"Alice", tests.ServiceAccountEmail, "Jane", "janedoe@gmail.com"}
|
||||
return createStatements, insertStatement, toolStatement, params
|
||||
}
|
||||
|
||||
func getFirebirdWants() (string, string, string) {
|
||||
select1Want := `[{"constant":1}]`
|
||||
failInvocationWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: Dynamic SQL Error\nSQL error code = -104\nToken unknown - line 1, column 1\nSELEC\n"}],"isError":true}}`
|
||||
createTableStatement := `"CREATE TABLE t (id INTEGER PRIMARY KEY, name VARCHAR(50))"`
|
||||
return select1Want, failInvocationWant, createTableStatement
|
||||
}
|
||||
|
||||
func getFirebirdToolsConfig(sourceConfig map[string]any, toolKind, paramToolStatement, idParamToolStmt, nameParamToolStmt, arrayToolStatement, authToolStatement string) map[string]any {
|
||||
toolsFile := tests.GetToolsConfig(sourceConfig, toolKind, paramToolStatement, idParamToolStmt, nameParamToolStmt, arrayToolStatement, authToolStatement)
|
||||
|
||||
toolsMap, ok := toolsFile["tools"].(map[string]any)
|
||||
if !ok {
|
||||
return toolsFile
|
||||
}
|
||||
|
||||
if simpleTool, ok := toolsMap["my-simple-tool"].(map[string]any); ok {
|
||||
simpleTool["statement"] = "SELECT 1 AS \"constant\" FROM RDB$DATABASE;"
|
||||
toolsMap["my-simple-tool"] = simpleTool
|
||||
}
|
||||
if authRequiredTool, ok := toolsMap["my-auth-required-tool"].(map[string]any); ok {
|
||||
authRequiredTool["statement"] = "SELECT 1 AS \"constant\" FROM RDB$DATABASE;"
|
||||
toolsMap["my-auth-required-tool"] = authRequiredTool
|
||||
}
|
||||
|
||||
if arrayTool, ok := toolsMap["my-array-tool"].(map[string]any); ok {
|
||||
// Firebird array tool - accept array but use only first element for compatibility
|
||||
arrayTool["parameters"] = []any{
|
||||
map[string]any{
|
||||
"name": "idArray",
|
||||
"type": "array",
|
||||
"description": "ID array (Firebird will use first element only)",
|
||||
"items": map[string]any{
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"description": "ID",
|
||||
},
|
||||
},
|
||||
}
|
||||
// Statement is already defined in arrayToolStatement parameter
|
||||
toolsMap["my-array-tool"] = arrayTool
|
||||
}
|
||||
|
||||
toolsFile["tools"] = toolsMap
|
||||
return toolsFile
|
||||
}
|
||||
|
||||
func addFirebirdTemplateParamConfig(t *testing.T, config map[string]any, toolKind, tmplSelectCombined, tmplSelectFilterCombined string) map[string]any {
|
||||
toolsMap, ok := config["tools"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("unable to get tools from config")
|
||||
}
|
||||
|
||||
// Firebird-specific template parameter tools with compatible syntax
|
||||
toolsMap["create-table-templateParams-tool"] = map[string]any{
|
||||
"kind": toolKind,
|
||||
"source": "my-instance",
|
||||
"description": "Create table tool with template parameters",
|
||||
"statement": "CREATE TABLE {{.tableName}} ({{array .columns}})",
|
||||
"templateParameters": []tools.Parameter{
|
||||
tools.NewStringParameter("tableName", "some description"),
|
||||
tools.NewArrayParameter("columns", "The columns to create", tools.NewStringParameter("column", "A column name that will be created")),
|
||||
},
|
||||
}
|
||||
toolsMap["insert-table-templateParams-tool"] = map[string]any{
|
||||
"kind": toolKind,
|
||||
"source": "my-instance",
|
||||
"description": "Insert table tool with template parameters",
|
||||
"statement": "INSERT INTO {{.tableName}} ({{array .columns}}) VALUES ({{.values}})",
|
||||
"templateParameters": []tools.Parameter{
|
||||
tools.NewStringParameter("tableName", "some description"),
|
||||
tools.NewArrayParameter("columns", "The columns to insert into", tools.NewStringParameter("column", "A column name that will be returned from the query.")),
|
||||
tools.NewStringParameter("values", "The values to insert as a comma separated string"),
|
||||
},
|
||||
}
|
||||
toolsMap["select-templateParams-tool"] = map[string]any{
|
||||
"kind": toolKind,
|
||||
"source": "my-instance",
|
||||
"description": "Select table tool with template parameters",
|
||||
"statement": "SELECT id AS \"id\", name AS \"name\", age AS \"age\" FROM {{.tableName}}",
|
||||
"templateParameters": []tools.Parameter{
|
||||
tools.NewStringParameter("tableName", "some description"),
|
||||
},
|
||||
}
|
||||
toolsMap["select-templateParams-combined-tool"] = map[string]any{
|
||||
"kind": toolKind,
|
||||
"source": "my-instance",
|
||||
"description": "Select table tool with combined template parameters",
|
||||
"statement": tmplSelectCombined,
|
||||
"parameters": []tools.Parameter{
|
||||
tools.NewIntParameter("id", "the id of the user"),
|
||||
},
|
||||
"templateParameters": []tools.Parameter{
|
||||
tools.NewStringParameter("tableName", "some description"),
|
||||
},
|
||||
}
|
||||
toolsMap["select-fields-templateParams-tool"] = map[string]any{
|
||||
"kind": toolKind,
|
||||
"source": "my-instance",
|
||||
"description": "Select specific fields tool with template parameters",
|
||||
"statement": "SELECT name AS \"name\" FROM {{.tableName}}",
|
||||
"templateParameters": []tools.Parameter{
|
||||
tools.NewStringParameter("tableName", "some description"),
|
||||
},
|
||||
}
|
||||
toolsMap["select-filter-templateParams-combined-tool"] = map[string]any{
|
||||
"kind": toolKind,
|
||||
"source": "my-instance",
|
||||
"description": "Select table tool with filter template parameters",
|
||||
"statement": tmplSelectFilterCombined,
|
||||
"parameters": []tools.Parameter{
|
||||
tools.NewStringParameter("name", "the name to filter by"),
|
||||
},
|
||||
"templateParameters": []tools.Parameter{
|
||||
tools.NewStringParameter("tableName", "some description"),
|
||||
tools.NewStringParameter("columnFilter", "some description"),
|
||||
},
|
||||
}
|
||||
// Firebird uses simple DROP TABLE syntax without IF EXISTS
|
||||
toolsMap["drop-table-templateParams-tool"] = map[string]any{
|
||||
"kind": toolKind,
|
||||
"source": "my-instance",
|
||||
"description": "Drop table tool with template parameters",
|
||||
"statement": "DROP TABLE {{.tableName}}",
|
||||
"templateParameters": []tools.Parameter{
|
||||
tools.NewStringParameter("tableName", "some description"),
|
||||
},
|
||||
}
|
||||
config["tools"] = toolsMap
|
||||
return config
|
||||
}
|
||||
|
||||
func addFirebirdExecuteSqlConfig(t *testing.T, config map[string]any) map[string]any {
|
||||
tools, ok := config["tools"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("unable to get tools from config")
|
||||
}
|
||||
tools["my-exec-sql-tool"] = map[string]any{
|
||||
"kind": "firebird-execute-sql",
|
||||
"source": "my-instance",
|
||||
"description": "Tool to execute sql",
|
||||
}
|
||||
tools["my-auth-exec-sql-tool"] = map[string]any{
|
||||
"kind": "firebird-execute-sql",
|
||||
"source": "my-instance",
|
||||
"description": "Tool to execute sql",
|
||||
"authRequired": []string{
|
||||
"my-google-auth",
|
||||
},
|
||||
}
|
||||
config["tools"] = tools
|
||||
return config
|
||||
}
|
||||
|
||||
func getFirebirdTmplToolStatement() (string, string) {
|
||||
tmplSelectCombined := "SELECT id AS \"id\", name AS \"name\", age AS \"age\" FROM {{.tableName}} WHERE id = ?"
|
||||
tmplSelectFilterCombined := "SELECT id AS \"id\", name AS \"name\", age AS \"age\" FROM {{.tableName}} WHERE {{.columnFilter}} = ?"
|
||||
return tmplSelectCombined, tmplSelectFilterCombined
|
||||
}
|
||||
Reference in New Issue
Block a user