Compare commits

...

4 Commits

Author SHA1 Message Date
Ben Knutson
41b04ea66c Refactor Github Action per b/485167538 2026-02-18 02:51:28 +00:00
Yuan Teoh
a21d9a158b consolidate to one workflow 2026-01-05 17:41:18 -08:00
Yuan Teoh
2618bd7673 ci: add gemini bot for issue triage 2025-12-18 11:15:02 -08:00
Wenxin Du
8ea39ec32f feat(sources/oracle): Add Oracle OCI and Wallet support (#1945)
Previously we used go-ora (a pure Go Oracle driver) because our release
pipeline did not support cross-compilation with CGO. Now that it's
fixed, we want to add support for Oracle OCI driver for advanced
features including digital wallet etc.

Users will be able to configure a source to use OCI by specifying a
`UseOCI: true` field. The source defaults to use the pure Go driver
otherwise.

Oracle Wallet:
- OCI users should use the `tnsAdmin` to set the wallet location
- Non-OCI users can should use the `walletLocation` field.

fix: https://github.com/googleapis/genai-toolbox/issues/1779
2025-12-18 19:02:17 +00:00
15 changed files with 958 additions and 50 deletions

View File

@@ -305,4 +305,4 @@ substitutions:
_AR_HOSTNAME: ${_REGION}-docker.pkg.dev
_AR_REPO_NAME: toolbox-dev
_BUCKET_NAME: genai-toolbox-dev
_DOCKER_URI: ${_AR_HOSTNAME}/${PROJECT_ID}/${_AR_REPO_NAME}/toolbox
_DOCKER_URI: ${_AR_HOSTNAME}/${PROJECT_ID}/${_AR_REPO_NAME}/toolbox

View File

@@ -846,8 +846,8 @@ steps:
cassandra
- id: "oracle"
name: golang:1
waitFor: ["compile-test-binary"]
name: ghcr.io/oracle/oraclelinux9-instantclient:23
waitFor: ["install-dependencies"]
entrypoint: /bin/bash
env:
- "GOPATH=/gopath"
@@ -860,10 +860,25 @@ steps:
args:
- -c
- |
.ci/test_with_coverage.sh \
"Oracle" \
oracle \
oracle
# Install the C compiler and Oracle SDK headers needed for cgo
dnf install -y gcc oracle-instantclient-devel
# Install Go
curl -L -o go.tar.gz "https://go.dev/dl/go1.25.1.linux-amd64.tar.gz"
tar -C /usr/local -xzf go.tar.gz
export PATH="/usr/local/go/bin:$$PATH"
go test -v ./internal/sources/oracle/... \
-coverprofile=oracle_coverage.out \
-coverpkg=./internal/sources/oracle/...,./internal/tools/oracle/...
# Coverage check
total_coverage=$(go tool cover -func=oracle_coverage.out | grep "total:" | awk '{print $3}')
echo "Oracle total coverage: $total_coverage"
coverage_numeric=$(echo "$total_coverage" | sed 's/%//')
if awk -v cov="$coverage_numeric" 'BEGIN {exit !(cov < 30)}'; then
echo "Coverage failure: $total_coverage is below 30%."
exit 1
fi
- id: "serverless-spark"
name: golang:1

10
.github/labels.yaml vendored
View File

@@ -83,10 +83,16 @@
- name: 'status: feedback wanted'
color: 8befd7
description: 'Status: waiting for feedback from community or issue author.'
- name: 'status: waiting for response'
color: 8befd7
description: 'Status: reviewer is awaiting feedback or responses from the author before proceeding.'
- name: 'status: need-triage'
color: 8befd7
description: 'Status: Issues that needs to be triaged by the triage automation.'
- name: 'status: manual-triage'
color: 8befd7
description: 'Status: Issues that needs to be triaged by the maintainers.'
- name: 'release candidate'
color: 32CD32
@@ -179,4 +185,4 @@
description: 'Valkey'
- name: 'product: yugabytedb'
color: 5065c7
description: 'YugabyteDB'
description: 'YugabyteDB'

View File

@@ -35,7 +35,9 @@ jobs:
ref: ${{ github.event.release.tag_name }}
- name: Get Version from Release Tag
run: echo "VERSION=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
run: echo "VERSION=${GITHUB_EVENT_RELEASE_TAG_NAME}" >> $GITHUB_ENV
env:
GITHUB_EVENT_RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
- name: Setup Hugo
uses: peaceiris/actions-hugo@75d2e84710de30f6ff7268e08f310b60ef14033f # v3

View File

@@ -0,0 +1,396 @@
# 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.
name: '🏷️ Gemini Issue Triage'
on:
schedule:
- cron: '0 0 * * *' # Runs everyday at midnight
issues:
types:
- 'opened' # automated triage when issue opened
workflow_dispatch: # manually dispatch workflow
inputs:
issue_number:
description: 'issue number to triage'
required: false # set to false so can manually run bulk scan as well
type: 'number'
concurrency:
group: '${{ github.workflow }}-${{ github.event.issue.number || github.event.inputs.issue_number || scheduled }}'
cancel-in-progress: true
defaults:
run:
shell: 'bash'
permissions:
contents: 'read'
id-token: 'write'
issues: 'write'
statuses: 'write'
packages: 'read'
actions: 'write' # Required for cancelling a workflow run
jobs:
triage-issue:
if: |-
github.repository == 'googleapis/genai-toolbox' && !contains(github.event.issue.labels.*.name, 'priority:')
timeout-minutes: 10
runs-on: 'ubuntu-latest'
steps:
- name: 'Get issue data for manual trigger'
id: 'get_issue_data'
if: |-
github.event_name == 'workflow_dispatch'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
script: |
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ github.event.inputs.issue_number }},
});
core.setOutput('title', issue.title);
core.setOutput('body', issue.body);
core.setOutput('labels', issue.labels.map(label => label.name).join(','));
return issue;
- name: 'Manual Trigger Pre-flight Checks'
if: |-
github.event_name == 'workflow_dispatch'
env:
ISSUE_NUMBER_INPUT: '${{ github.event.inputs.issue_number }}'
LABELS: '${{ steps.get_issue_data.outputs.labels }}'
run: |
if echo "${LABELS}" | grep -q 'priority:'; then
echo "Issue #${ISSUE_NUMBER_INPUT} already has 'priority:' labels. Stopping workflow."
exit 1
fi
echo "Manual triage checks passed."
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
- name: 'Get Repository Labels'
id: 'get_labels'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |-
// Fetch ALL labels (handling pagination automatically)
const labels = await github.paginate(github.rest.issues.listLabelsForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100
});
// Only grab labels with specific prefix
const targetPrefixes = ['priority:', 'product:', 'type:'];
const labelNames = labels.map(label => label.name).filter(name =>
targetPrefixes.some(prefix => name.startsWith(prefix)));
// Export labels
core.setOutput('available_labels', labelNames.join(','));
core.info(`Found ${labelNames.length} labels: ${labelNames.join(', ')}`);
return labelNames;
- name: 'Find untriaged issues'
id: 'find_issues'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: '${{ github.repository }}'
ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
run: |-
set -euo pipefail
ISSUES="[]"
if [[ -n "${ISSUE_NUMBER}" ]]; then
echo "🎯 Single Issue Mode: Processing #${ISSUE_NUMBER}..."
SINGLE_DATA="$(gh issue view "${ISSUE_NUMBER}" \
--repo "${GITHUB_REPOSITORY}" \
--json number,title,body)"
ISSUES="[${SINGLE_DATA}]"
else
echo "📅 Bulk Mode: Running full triage scan..."
echo '🔍 Finding issues without labels...'
NO_LABEL_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
--search 'is:open is:issue no:label' --json number,title,body)"
echo '🏷️ Finding issues that need triage...'
NEED_TRIAGE_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
--search "is:open is:issue label:\"status: need-triage\" -label:\"status: manual-triage\"" --limit 1000 --json number,title,body)"
echo '🔄 Merging and deduplicating issues...'
ISSUES="$(echo "${NO_LABEL_ISSUES}" "${NEED_TRIAGE_ISSUES}" | jq -c -s 'add | unique_by(.number)')"
fi
echo '📝 Setting output for GitHub Actions...'
echo "issues_to_triage=${ISSUES}" >> "${GITHUB_OUTPUT}"
ISSUE_COUNT="$(echo "${ISSUES}" | jq 'length')"
echo "✅ Found ${ISSUE_COUNT} issues to triage! 🎯"
- name: 'Run Gemini Issue Analysis'
if: |- # skip workflow if its a scheduled workflow without any issues to triage
${{ !(github.event_name == 'schedule' &&
steps.find_issues.outputs.issues_to_triage == '[]') }}
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
id: 'gemini_issue_analysis'
env:
GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs
ISSUES_TO_TRIAGE: '${{ steps.find_issues.outputs.issues_to_triage }}'
REPOSITORY: '${{ github.repository }}'
AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
with:
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
settings: |-
{
"maxSessionTurns": 25,
"telemetry": {
"enabled": true,
"target": "gcp"
}
}
prompt: |-
## Role
You are an issue triage assistant. Your role is to analyze a GitHub
issue and identify appropriate labels based on the definitions
provided.
## Steps
1. Check environment variable for issues to triage: $ISSUES_TO_TRIAGE (JSON array of issues).
2. Review the available labels: ${{ env.AVAILABLE_LABELS }}.
3. Identify the most relevant labels from the existing labels,
focusing on 'priority: *', 'type: *', and 'product: *'.
4. If the issue already has a 'product: *' label, do not try to
change it. If the issue already has a 'type: *' label, do not try to
change it. If the issue already has a 'priority: *' label, do not
try to change it. For example, if an issue already has a 'product:
*' label, you wil only add a 'type: *' and/or 'priority: *' label.
Instead, if an issue has no labels, you could add one labels of each
kind.
5. Fallback Logic:
- If you cannot confidently determine the correct 'product: *' label
from the definitions, feel free to leave it.
- If you cannot confidently determine the correct 'type: *' label
from the definitions, feel free to leave it.
- If you cannot confidently determine the correct 'priority: *'
label from the definitions, apply the 'status: manual-triage'
label.
6. Give me a single short explanation about why you are selecting
each label in the process.
7. Output a JSON array of objects, each containing the issue number
and the labels to add and remove, along with an explanation and
nothing else. Example:
```
[
{
"issue_number": 123,
"labels_to_add": ["product: alloydb", "priority: p2"],
"labels_to_remove": ["status: need-triage"],
"explanation": "This issue is a bug within the alloydb tool that needs to be addressed with medium priority."
}
]
```
8. If you see that the issue doesn't look like it has sufficient
information, leave a comment politely requesting the relevant
information.
- After identifying appropriate labels to an issue, add "status:
need-triage" label to labels_to_remove in the output.
10. If you think an issue might be a 'priority: p0' do not apply the
'priority: p0' label. Instead, apply a 'status: manual-triage' label
and include a note in your explanation.
## Guidelines
- Your output must contain exactly one priority: label.
- Output only valid JSON format.
- Do not include any explanation or additional text, just the JSON.
- Only use labels that already exist in the repository.
- Do not add comments or modify the issue content.
- Triage only the current issue.
- Identify only one 'product: *' label
- Identify applicable 'priority: *' labels based on the issue content.
- Once you categorize the issue if it needs information, bump down the priority by 1 eg.. a p0 would become a p1 a p1 would become a p2. P2 and P3 can stay as is in this scenario.
Guidelines for Priority labels
'priority: p0': Critical / Blocker
- Definition: A catastrophic failure that makes the server unusable for most users or poses a severe security risk. This includes installation failures, authentication failures, persistent crashes, or critical security vulnerabilities.
- Key Questions:
- Is the main goal of the tool (e.g., connecting an agent to a database) completely impossible?
- Is the server failing to install or run?
- Does this represent a critical security vulnerability?
- Does this block existing user and have to be resolved immediately in order to utilize the server again?
- Does this issue affect every user immediately upon running the latest version?
- Is there absolutely no temporary workaround or alternative method to achieve the desired result?
'priority: p1': High
- Definition: A severe issue that causes a significant degradation of a key feature, produces incorrect or inconsistent results, or severely impacts a large number of users. It requires prompt resolution, though a temporary workaround might exist. This also includes critical missing documentation for core features.
- Key Questions:
- Does this issue affect a key component that is widely relied upon (e.g., core database operations)?
- Are the results produced by the tool incorrect, misleading, or unreliable?
- Is a feature failing for a specific, large user group (e.g., all Windows users, all users of a specific shell)?
- Does a user need to perform difficult, undocumented steps to work around the problem?
- Is essential setup or usage documentation completely missing for a new feature?
'priority: p2': Medium
- Definition: A moderately impactful issue causing inconvenience or a non-optimal experience, but a reasonable workaround exists. This also includes failures in non-core features.
- Key Questions:
- Is the issue a standard bug fix that only affects a smaller, non-critical area of the code?
- Is this a clear, actionable enhancement that adds tangible value without being mission-critical?
- Can the user easily and reliably work around the issue without major difficulty?
- Is this an overdue technical debt item or a minor documentation correction?
'priority: p3': Low
- Definition: A minor, low-impact issue with minimal effect on functionality. This includes most cosmetic defects, typos in documentation, or unclear help text. They have minimal to no impact on the current functionality or user experience and can be addressed when time and resources allow.
- Key Questions:
- Is this a typo in the README.md, gemini --help text, or other documentation?
- Is this a minor cosmetic issue (e.g., text alignment in output, an extra newline) that doesn't affect usability?
- Is the issue a minor cleanup or refactoring that doesn't fix a current problem but improves code style?
- Can this be ignored for several release cycles without negatively impacting users?
Guidelines for Product labels
If the issue is specific towards a product, add the product label.
For example, alloydb related issue should be assigned the 'product:
alloydb' label. The available 'product: *' labels are included in
the list of available products.
Guidelines for Type labels
Assign the issue based on type. The available 'type: *' labels are
included in the list of available labels.
'type: bug'
- Error or flaw in code with unintended results or allowing sub-optimal usage patterns.
'type: cleanup'
- An internal cleanup or hygiene concern.
'type: docs'
- Improvement to the documentation for an API.
'type: feature request'
- Nice-to-have improvement, new feature or different behavior or design.
'type: process'
- A process-related concern. May include testing, release, or the like.
'type: question'
- Request for information or clarification.
- name: 'Apply Labels to Issue'
if: |-
${{ steps.gemini_issue_analysis.outcome == 'success' &&
steps.gemini_issue_analysis.outputs.summary != '[]' }}
env:
REPOSITORY: '${{ github.repository }}'
LABELS_OUTPUT: '${{ steps.gemini_issue_analysis.outputs.summary }}'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const rawOutput = process.env.LABELS_OUTPUT;
core.info(`Raw output from model: ${rawOutput}`);
let parsedLabels;
try {
const jsonMatch = rawLabels.match(/```json\s*([\s\S]*?)\s*```/);
if (!jsonMatch || !jsonMatch[1]) {
throw new Error("Could not find a ```json ... ``` block in the output.");
}
const jsonString = jsonMatch[1].trim();
parsedLabels = JSON.parse(jsonString);
core.info(`Parsed labels JSON: ${JSON.stringify(parsedLabels)}`);
} catch (err) {
core.setFailed(`Failed to parse labels JSON from Gemini output: ${err.message}\nRaw output: ${rawLabels}`);
return;
}
for (const entry of parsedLabels) {
const issueNumber = entry.issue_number;
if (!issueNumber) {
core.info(`Skipping entry with no issue number: ${JSON.stringify(entry)}`);
continue;
}
if (labelsToAdd.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: labelsToAdd
});
const explanation = entry.explanation ? ` - ${entry.explanation}` : '';
core.info(`Successfully added labels for #${issueNumber}: ${labelsToAdd.join(', ')}${explanation}`);
}
if (entry.labels_to_remove && entry.labels_to_remove.length > 0) {
for (const label of entry.labels_to_remove) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
name: label
});
} catch (error) {
if (error.status !== 404) {
throw error;
}
}
}
core.info(`Successfully removed labels for #${issueNumber}: ${entry.labels_to_remove.join(', ')}`);
}
if (entry.explanation) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: entry.explanation,
});
}
if ((!entry.labels_to_add || entry.labels_to_add.length === 0) && (!entry.labels_to_remove || entry.labels_to_remove.length === 0)) {
core.info(`No labels to add or remove for #${issueNumber}, leaving as is`);
}
}
- name: 'Post Issue Analysis Failure Comment' # only post failure comment for open issues and manual workflow dispatch
if: |-
${{
github.event_name != 'schedule' &&
failure() &&
steps.gemini_issue_analysis.outcome == 'failure'
}}
env:
ISSUES_TO_TRIAGE: '${{ steps.find_issues.outputs.issues_to_triage }}'
ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |-
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(process.env.ISSUE_NUMBER),
body: 'There is a problem with the Gemini CLI issue triaging. Please check the [action logs](${process.env.RUN_URL}) for details.'
})

View File

@@ -189,6 +189,10 @@ tools.
* **(Optional) Add samples** to the `docs/en/samples/<newdb>` directory.
### Updating labels
* Add a `product: <source>` label in `.github/labels.yaml`
### (Optional) Adding Prebuilt Tools
You can provide developers with a set of "build-time" tools to aid common

View File

@@ -18,10 +18,10 @@ DW) database workloads.
## Available Tools
- [`oracle-sql`](../tools/oracle/oracle-sql.md)
Execute pre-defined prepared SQL queries in Oracle.
Execute pre-defined prepared SQL queries in Oracle.
- [`oracle-execute-sql`](../tools/oracle/oracle-execute-sql.md)
Run parameterized SQL queries in Oracle.
Run parameterized SQL queries in Oracle.
## Requirements
@@ -33,6 +33,25 @@ user][oracle-users] to log in to the database with the necessary permissions.
[oracle-users]:
https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-USER.html
### Oracle Driver Requirement (Conditional)
The Oracle source offers two connection drivers:
1. **Pure Go Driver (`useOCI: false`, default):** Uses the `go-ora` library.
This driver is simpler and does not require any local Oracle software
installation, but it **lacks support for advanced features** like Oracle
Wallets or Kerberos authentication.
2. **OCI-Based Driver (`useOCI: true`):** Uses the `godror` library, which
provides access to **advanced Oracle features** like Digital Wallet support.
If you set `useOCI: true`, you **must** install the **Oracle Instant Client**
libraries on the machine where this tool runs.
You can download the Instant Client from the official Oracle website: [Oracle
Instant Client
Downloads](https://www.oracle.com/database/technologies/instant-client/downloads.html)
## Connection Methods
You can configure the connection to your Oracle database using one of the
@@ -66,12 +85,15 @@ using a TNS (Transparent Network Substrate) alias.
containing it. This setting will override the `TNS_ADMIN` environment
variable.
## Example
## Examples
This example demonstrates the four connection methods you could choose from:
```yaml
sources:
my-oracle-source:
kind: oracle
# --- Choose one connection method ---
# 1. Host, Port, and Service Name
host: 127.0.0.1
@@ -88,6 +110,43 @@ sources:
user: ${USER_NAME}
password: ${PASSWORD}
# Optional: Set to true to use the OCI-based driver for advanced features (Requires Oracle Instant Client)
```
### Using an Oracle Wallet
Oracle Wallet allows you to store credentails used for database connection. Depending whether you are using an OCI-based driver, the wallet configuration is different.
#### Pure Go Driver (`useOCI: false`) - Oracle Wallet
The `go-ora` driver uses the `walletLocation` field to connect to a database secured with an Oracle Wallet without standard username and password.
```yaml
sources:
pure-go-wallet:
kind: oracle
connectionString: "127.0.0.1:1521/XEPDB1"
user: ${USER_NAME}
password: ${PASSWORD}
# The TNS Alias is often required to connect to a service registered in tnsnames.ora
tnsAlias: "SECURE_DB_ALIAS"
walletLocation: "/path/to/my/wallet/directory"
```
#### OCI-Based Driver (`useOCI: true`) - Oracle Wallet
For the OCI-based driver, wallet authentication is triggered by setting tnsAdmin to the wallet directory and connecting via a tnsAlias.
```yaml
sources:
oci-wallet:
kind: oracle
connectionString: "127.0.0.1:1521/XEPDB1"
user: ${USER_NAME}
password: ${PASSWORD}
tnsAlias: "WALLET_DB_ALIAS"
tnsAdmin: "/opt/oracle/wallet" # Directory containing tnsnames.ora, sqlnet.ora, and wallet files
useOCI: true
```
{{< notice tip >}}
@@ -97,14 +156,15 @@ instead of hardcoding your secrets into the configuration file.
## Reference
| **field** | **type** | **required** | **description** |
|------------------|:--------:|:------------:|-----------------------------------------------------------------------------------------------------------------------------|
| kind | string | true | Must be "oracle". |
| user | string | true | Name of the Oracle user to connect as (e.g. "my-oracle-user"). |
| password | string | true | Password of the Oracle user (e.g. "my-password"). |
| host | string | false | IP address or hostname to connect to (e.g. "127.0.0.1"). Required if not using `connectionString` or `tnsAlias`. |
| port | integer | false | Port to connect to (e.g. "1521"). Required if not using `connectionString` or `tnsAlias`. |
| serviceName | string | false | The Oracle service name of the database to connect to. Required if not using `connectionString` or `tnsAlias`. |
| connectionString | string | false | A direct connection string (e.g. "hostname:port/servicename"). Use as an alternative to `host`, `port`, and `serviceName`. |
| tnsAlias | string | false | A TNS alias from a `tnsnames.ora` file. Use as an alternative to `host`/`port` or `connectionString`. |
| tnsAdmin | string | false | Path to the directory containing the `tnsnames.ora` file. This overrides the `TNS_ADMIN` environment variable if it is set. |
| **field** | **type** | **required** | **description** |
|------------------|:--------:|:------------:|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| kind | string | true | Must be "oracle". |
| user | string | true | Name of the Oracle user to connect as (e.g. "my-oracle-user"). |
| password | string | true | Password of the Oracle user (e.g. "my-password"). |
| host | string | false | IP address or hostname to connect to (e.g. "127.0.0.1"). Required if not using `connectionString` or `tnsAlias`. |
| port | integer | false | Port to connect to (e.g. "1521"). Required if not using `connectionString` or `tnsAlias`. |
| serviceName | string | false | The Oracle service name of the database to connect to. Required if not using `connectionString` or `tnsAlias`. |
| connectionString | string | false | A direct connection string (e.g. "hostname:port/servicename"). Use as an alternative to `host`, `port`, and `serviceName`. |
| tnsAlias | string | false | A TNS alias from a `tnsnames.ora` file. Use as an alternative to `host`/`port` or `connectionString`. |
| tnsAdmin | string | false | Path to the directory containing the `tnsnames.ora` file. This overrides the `TNS_ADMIN` environment variable if it is set. |
| useOCI | bool | false | If true, uses the OCI-based driver (godror) which supports Oracle Wallet/Kerberos but requires the Oracle Instant Client libraries to be installed. Defaults to false (pure Go driver). |

4
go.mod
View File

@@ -33,6 +33,7 @@ require (
github.com/go-playground/validator/v10 v10.28.0
github.com/go-sql-driver/mysql v1.9.3
github.com/goccy/go-yaml v1.18.0
github.com/godror/godror v0.49.4
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.6
@@ -91,6 +92,7 @@ require (
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/VictoriaMetrics/easyproto v0.1.4 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/apache/arrow/go/v15 v15.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -107,11 +109,13 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/godror/knownpb v0.3.0 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect

14
go.sum
View File

@@ -683,6 +683,10 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/UNO-SOFT/zlog v0.8.1 h1:TEFkGJHtUfTRgMkLZiAjLSHALjwSBdw6/zByMC5GJt4=
github.com/UNO-SOFT/zlog v0.8.1/go.mod h1:yqFOjn3OhvJ4j7ArJqQNA+9V+u6t9zSAyIZdWdMweWc=
github.com/VictoriaMetrics/easyproto v0.1.4 h1:r8cNvo8o6sR4QShBXQd1bKw/VVLSQma/V2KhTBPf+Sc=
github.com/VictoriaMetrics/easyproto v0.1.4/go.mod h1:QlGlzaJnDfFd8Lk6Ci/fuLxfTo3/GThPs2KH23mv710=
github.com/ahmetb/dlog v0.0.0-20170105205344-4fb5f8204f26 h1:3YVZUqkoev4mL+aCwVOSWV4M7pN+NURHL38Z2zq5JKA=
github.com/ahmetb/dlog v0.0.0-20170105205344-4fb5f8204f26/go.mod h1:ymXt5bw5uSNu4jveerFxE0vNYxF8ncqbptntMaFMg3k=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
@@ -884,6 +888,8 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -909,6 +915,10 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godror/godror v0.49.4 h1:8kKWKoR17nPX7u10hr4GwD4u10hzTZED9ihdkuzRrKI=
github.com/godror/godror v0.49.4/go.mod h1:kTMcxZzRw73RT5kn9v3JkBK4kHI6dqowHotqV72ebU8=
github.com/godror/knownpb v0.3.0 h1:+caUdy8hTtl7X05aPl3tdL540TvCcaQA6woZQroLZMw=
github.com/godror/knownpb v0.3.0/go.mod h1:PpTyfJwiOEAzQl7NtVCM8kdPCnp3uhxsZYIzZ5PV4zU=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
@@ -1172,6 +1182,8 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/neo4j/neo4j-go-driver/v5 v5.28.4 h1:7toxehVcYkZbyxV4W3Ib9VcnyRBQPucF+VwNNmtSXi4=
github.com/neo4j/neo4j-go-driver/v5 v5.28.4/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k=
github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc=
github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -1671,6 +1683,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -9,9 +9,11 @@ import (
"strings"
"github.com/goccy/go-yaml"
_ "github.com/godror/godror" // OCI driver
_ "github.com/sijms/go-ora/v2" // Pure Go driver
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/util"
_ "github.com/sijms/go-ora/v2"
"go.opentelemetry.io/otel/trace"
)
@@ -32,7 +34,7 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources
return nil, err
}
// Validate that we have one of: tns_alias, connection_string, or host+service_name
// Validate that we have one of: tnsAlias, connectionString, or host+service_name
if err := actual.validate(); err != nil {
return nil, fmt.Errorf("invalid Oracle configuration: %w", err)
}
@@ -43,21 +45,24 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources
type Config struct {
Name string `yaml:"name" validate:"required"`
Kind string `yaml:"kind" validate:"required"`
ConnectionString string `yaml:"connectionString,omitempty"` // Direct connection string (hostname[:port]/servicename)
TnsAlias string `yaml:"tnsAlias,omitempty"` // TNS alias from tnsnames.ora
Host string `yaml:"host,omitempty"` // Optional when using connectionString/tnsAlias
Port int `yaml:"port,omitempty"` // Explicit port support
ServiceName string `yaml:"serviceName,omitempty"` // Optional when using connectionString/tnsAlias
ConnectionString string `yaml:"connectionString,omitempty"`
TnsAlias string `yaml:"tnsAlias,omitempty"`
TnsAdmin string `yaml:"tnsAdmin,omitempty"`
Host string `yaml:"host,omitempty"`
Port int `yaml:"port,omitempty"`
ServiceName string `yaml:"serviceName,omitempty"`
User string `yaml:"user" validate:"required"`
Password string `yaml:"password" validate:"required"`
TnsAdmin string `yaml:"tnsAdmin,omitempty"` // Optional: override TNS_ADMIN environment variable
UseOCI bool `yaml:"useOCI,omitempty"`
WalletLocation string `yaml:"walletLocation,omitempty"`
}
// validate ensures we have one of: tns_alias, connection_string, or host+service_name
func (c Config) validate() error {
hasTnsAdmin := strings.TrimSpace(c.TnsAdmin) != ""
hasTnsAlias := strings.TrimSpace(c.TnsAlias) != ""
hasConnStr := strings.TrimSpace(c.ConnectionString) != ""
hasHostService := strings.TrimSpace(c.Host) != "" && strings.TrimSpace(c.ServiceName) != ""
hasWallet := strings.TrimSpace(c.WalletLocation) != ""
connectionMethods := 0
if hasTnsAlias {
@@ -78,6 +83,14 @@ func (c Config) validate() error {
return fmt.Errorf("provide only one connection method: 'tns_alias', 'connection_string', or 'host'+'service_name'")
}
if hasTnsAdmin && !c.UseOCI {
return fmt.Errorf("`tnsAdmin` can only be used when `UseOCI` is true, or use `walletLocation` instead")
}
if hasWallet && c.UseOCI {
return fmt.Errorf("when using an OCI driver, use `tnsAdmin` to specify credentials file location instead")
}
return nil
}
@@ -132,7 +145,8 @@ func initOracleConnection(ctx context.Context, tracer trace.Tracer, config Confi
panic(err)
}
// Set TNS_ADMIN environment variable if specified in config.
hasWallet := strings.TrimSpace(config.WalletLocation) != ""
if config.TnsAdmin != "" {
originalTnsAdmin := os.Getenv("TNS_ADMIN")
os.Setenv("TNS_ADMIN", config.TnsAdmin)
@@ -147,28 +161,49 @@ func initOracleConnection(ctx context.Context, tracer trace.Tracer, config Confi
}()
}
var serverString string
var connectStringBase string
if config.TnsAlias != "" {
// Use TNS alias
serverString = strings.TrimSpace(config.TnsAlias)
connectStringBase = strings.TrimSpace(config.TnsAlias)
} else if config.ConnectionString != "" {
// Use provided connection string directly (hostname[:port]/servicename format)
serverString = strings.TrimSpace(config.ConnectionString)
connectStringBase = strings.TrimSpace(config.ConnectionString)
} else {
// Build connection string from host and service_name
if config.Port > 0 {
serverString = fmt.Sprintf("%s:%d/%s", config.Host, config.Port, config.ServiceName)
connectStringBase = fmt.Sprintf("%s:%d/%s", config.Host, config.Port, config.ServiceName)
} else {
serverString = fmt.Sprintf("%s/%s", config.Host, config.ServiceName)
connectStringBase = fmt.Sprintf("%s/%s", config.Host, config.ServiceName)
}
}
connStr := fmt.Sprintf("oracle://%s:%s@%s",
config.User, config.Password, serverString)
var driverName string
var finalConnStr string
db, err := sql.Open("oracle", connStr)
if config.UseOCI {
// Use godror driver (requires OCI)
driverName = "godror"
finalConnStr = fmt.Sprintf(`user="%s" password="%s" connectString="%s"`,
config.User, config.Password, connectStringBase)
logger.DebugContext(ctx, fmt.Sprintf("Using godror driver (OCI-based) with connectString: %s\n", connectStringBase))
} else {
// Use go-ora driver (pure Go)
driverName = "oracle"
user := config.User
password := config.Password
if hasWallet {
finalConnStr = fmt.Sprintf("oracle://%s:%s@%s?ssl=true&wallet=%s",
user, password, connectStringBase, config.WalletLocation)
} else {
// Standard go-ora connection
finalConnStr = fmt.Sprintf("oracle://%s:%s@%s",
config.User, config.Password, connectStringBase)
logger.DebugContext(ctx, fmt.Sprintf("Using go-ora driver (pure-Go) with serverString: %s\n", connectStringBase))
}
}
db, err := sql.Open(driverName, finalConnStr)
if err != nil {
return nil, fmt.Errorf("unable to open Oracle connection: %w", err)
return nil, fmt.Errorf("unable to open Oracle connection with driver %s: %w", driverName, err)
}
return db, nil

View File

@@ -0,0 +1,200 @@
// Copyright © 2025, Oracle and/or its affiliates.
package oracle_test
import (
"strings"
"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/oracle"
"github.com/googleapis/genai-toolbox/internal/testutils"
)
func TestParseFromYamlOracle(t *testing.T) {
tcs := []struct {
desc string
in string
want server.SourceConfigs
}{
{
desc: "connection string and useOCI=true",
in: `
sources:
my-oracle-cs:
kind: oracle
connectionString: "my-host:1521/XEPDB1"
user: my_user
password: my_pass
useOCI: true
`,
want: server.SourceConfigs{
"my-oracle-cs": oracle.Config{
Name: "my-oracle-cs",
Kind: oracle.SourceKind,
ConnectionString: "my-host:1521/XEPDB1",
User: "my_user",
Password: "my_pass",
UseOCI: true,
},
},
},
{
desc: "host/port/serviceName and default useOCI=false",
in: `
sources:
my-oracle-host:
kind: oracle
host: my-host
port: 1521
serviceName: ORCLPDB
user: my_user
password: my_pass
`,
want: server.SourceConfigs{
"my-oracle-host": oracle.Config{
Name: "my-oracle-host",
Kind: oracle.SourceKind,
Host: "my-host",
Port: 1521,
ServiceName: "ORCLPDB",
User: "my_user",
Password: "my_pass",
UseOCI: false,
},
},
},
{
desc: "tnsAlias and TnsAdmin specified with explicit useOCI=true",
in: `
sources:
my-oracle-tns-oci:
kind: oracle
tnsAlias: FINANCE_DB
tnsAdmin: /opt/oracle/network/admin
user: my_user
password: my_pass
useOCI: true
`,
want: server.SourceConfigs{
"my-oracle-tns-oci": oracle.Config{
Name: "my-oracle-tns-oci",
Kind: oracle.SourceKind,
TnsAlias: "FINANCE_DB",
TnsAdmin: "/opt/oracle/network/admin",
User: "my_user",
Password: "my_pass",
UseOCI: true,
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
got := struct {
Sources server.SourceConfigs `yaml:"sources"`
}{}
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:\nwant: %v\ngot: %v\ndiff: %s", tc.want, got.Sources, cmp.Diff(tc.want, got.Sources))
}
})
}
}
func TestFailParseFromYamlOracle(t *testing.T) {
tcs := []struct {
desc string
in string
err string
}{
{
desc: "extra field",
in: `
sources:
my-oracle-instance:
kind: oracle
host: my-host
serviceName: ORCL
user: my_user
password: my_pass
extraField: value
`,
err: "unable to parse source \"my-oracle-instance\" as \"oracle\": [1:1] unknown field \"extraField\"\n> 1 | extraField: value\n ^\n 2 | host: my-host\n 3 | kind: oracle\n 4 | password: my_pass\n 5 | ",
},
{
desc: "missing required password field",
in: `
sources:
my-oracle-instance:
kind: oracle
host: my-host
serviceName: ORCL
user: my_user
`,
err: "unable to parse source \"my-oracle-instance\" as \"oracle\": Key: 'Config.Password' Error:Field validation for 'Password' failed on the 'required' tag",
},
{
desc: "missing connection method fields (validate fails)",
in: `
sources:
my-oracle-instance:
kind: oracle
user: my_user
password: my_pass
`,
err: "unable to parse source \"my-oracle-instance\" as \"oracle\": invalid Oracle configuration: must provide one of: 'tns_alias', 'connection_string', or both 'host' and 'service_name'",
},
{
desc: "multiple connection methods provided (validate fails)",
in: `
sources:
my-oracle-instance:
kind: oracle
host: my-host
serviceName: ORCL
connectionString: "my-host:1521/XEPDB1"
user: my_user
password: my_pass
`,
err: "unable to parse source \"my-oracle-instance\" as \"oracle\": invalid Oracle configuration: provide only one connection method: 'tns_alias', 'connection_string', or 'host'+'service_name'",
},
{
desc: "fail on tnsAdmin with useOCI=false",
in: `
sources:
my-oracle-fail:
kind: oracle
tnsAlias: FINANCE_DB
tnsAdmin: /opt/oracle/network/admin
user: my_user
password: my_pass
useOCI: false
`,
err: "unable to parse source \"my-oracle-fail\" as \"oracle\": invalid Oracle configuration: `tnsAdmin` can only be used when `UseOCI` is true, or use `walletLocation` instead",
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
got := struct {
Sources server.SourceConfigs `yaml:"sources"`
}{}
err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got)
if err == nil {
t.Fatalf("expect parsing to fail")
}
errStr := strings.ReplaceAll(err.Error(), "\r", "")
if errStr != tc.err {
t.Fatalf("unexpected error:\ngot:\n%q\nwant:\n%q\n", errStr, tc.err)
}
})
}
}

View File

@@ -110,7 +110,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
if err != nil {
return nil, fmt.Errorf("error getting logger: %s", err)
}
logger.DebugContext(ctx, fmt.Sprintf("executing `%s` tool query: %s", kind, sqlParam))
logger.DebugContext(ctx, "executing `%s` tool query: %s", kind, sqlParam)
results, err := t.Pool.QueryContext(ctx, sqlParam)
if err != nil {

View File

@@ -0,0 +1,82 @@
// Copyright © 2025, Oracle and/or its affiliates.
package oracleexecutesql_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/oracle/oracleexecutesql"
)
func TestParseFromYamlOracleExecuteSql(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 with auth",
in: `
tools:
run_adhoc_query:
kind: oracle-execute-sql
source: my-oracle-instance
description: Executes arbitrary SQL statements like INSERT or UPDATE.
authRequired:
- my-google-auth-service
`,
want: server.ToolConfigs{
"run_adhoc_query": oracleexecutesql.Config{
Name: "run_adhoc_query",
Kind: "oracle-execute-sql",
Source: "my-oracle-instance",
Description: "Executes arbitrary SQL statements like INSERT or UPDATE.",
AuthRequired: []string{"my-google-auth-service"},
},
},
},
{
desc: "example without authRequired",
in: `
tools:
run_simple_update:
kind: oracle-execute-sql
source: db-dev
description: Runs a simple update operation.
`,
want: server.ToolConfigs{
"run_simple_update": oracleexecutesql.Config{
Name: "run_simple_update",
Kind: "oracle-execute-sql",
Source: "db-dev",
Description: "Runs a simple update operation.",
AuthRequired: []string{},
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
got := struct {
Tools server.ToolConfigs `yaml:"tools"`
}{}
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)
}
})
}
}

View File

@@ -0,0 +1,85 @@
// Copyright © 2025, Oracle and/or its affiliates.
package oraclesql_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/oracle/oraclesql"
)
func TestParseFromYamlOracleSql(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 with statement and auth",
in: `
tools:
get_user_by_id:
kind: oracle-sql
source: my-oracle-instance
description: Retrieves user details by ID.
statement: "SELECT id, name, email FROM users WHERE id = :1"
authRequired:
- my-google-auth-service
`,
want: server.ToolConfigs{
"get_user_by_id": oraclesql.Config{
Name: "get_user_by_id",
Kind: "oracle-sql",
Source: "my-oracle-instance",
Description: "Retrieves user details by ID.",
Statement: "SELECT id, name, email FROM users WHERE id = :1",
AuthRequired: []string{"my-google-auth-service"},
},
},
},
{
desc: "example with parameters and template parameters",
in: `
tools:
get_orders:
kind: oracle-sql
source: db-prod
description: Gets orders for a customer with optional filtering.
statement: "SELECT * FROM ${SCHEMA}.ORDERS WHERE customer_id = :customer_id AND status = :status"
`,
want: server.ToolConfigs{
"get_orders": oraclesql.Config{
Name: "get_orders",
Kind: "oracle-sql",
Source: "db-prod",
Description: "Gets orders for a customer with optional filtering.",
Statement: "SELECT * FROM ${SCHEMA}.ORDERS WHERE customer_id = :customer_id AND status = :status",
AuthRequired: []string{},
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
got := struct {
Tools server.ToolConfigs `yaml:"tools"`
}{}
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)
}
})
}
}

View File

@@ -43,6 +43,7 @@ func getOracleVars(t *testing.T) map[string]any {
return map[string]any{
"kind": OracleSourceKind,
"connectionString": OracleConnStr,
"useOCI": true,
"user": OracleUser,
"password": OraclePass,
}
@@ -50,9 +51,11 @@ func getOracleVars(t *testing.T) map[string]any {
// Copied over from oracle.go
func initOracleConnection(ctx context.Context, user, pass, connStr string) (*sql.DB, error) {
fullConnStr := fmt.Sprintf("oracle://%s:%s@%s", user, pass, connStr)
// Build the full Oracle connection string for godror driver
fullConnStr := fmt.Sprintf(`user="%s" password="%s" connectString="%s"`,
user, pass, connStr)
db, err := sql.Open("oracle", fullConnStr)
db, err := sql.Open("godror", fullConnStr)
if err != nil {
return nil, fmt.Errorf("unable to open Oracle connection: %w", err)
}
@@ -116,13 +119,15 @@ func TestOracleSimpleToolEndpoints(t *testing.T) {
// Get configs for tests
select1Want := "[{\"1\":1}]"
mcpMyFailToolWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: ORA-00900: invalid SQL statement\n error occur at position: 0"}],"isError":true}}`
mcpMyFailToolWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: dpiStmt_execute: ORA-00900: invalid SQL statement"}],"isError":true}}`
createTableStatement := `"CREATE TABLE t (id NUMBER GENERATED AS IDENTITY PRIMARY KEY, name VARCHAR2(255))"`
mcpSelect1Want := `{"jsonrpc":"2.0","id":"invoke my-auth-required-tool","result":{"content":[{"type":"text","text":"{\"1\":1}"}]}}`
// Run tests
tests.RunToolGetTest(t)
tests.RunToolInvokeTest(t, select1Want,
tests.DisableOptionalNullParamTest(),
tests.WithMyToolById4Want("[{\"id\":4,\"name\":\"\"}]"),
tests.DisableArrayTest(),
)
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want)