Skip to content
This repository was archived by the owner on May 7, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/skills-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Skills Version Check

on:
pull_request:
paths:
- "skills/**"

jobs:
version-parity:
name: Check skills/versions.json parity
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Verify versions.json matches SKILL.md frontmatter
run: bash skills/check-versions.sh
3 changes: 3 additions & 0 deletions ibis-server/app/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

# Validate the dto by building the specific connection info from the data source
def verify_query_dto(data_source: DataSource, dto: QueryDTO):
# Skip inline validation when using a file path; connection info is validated at query time
if dto.connection_file_path:
return
# Use data_source.get_connection_info to validate the connection_info
# This will ensure the connection_info can be properly parsed for the specific data source
data_source.get_connection_info(dto.connection_info, {})
Expand Down
40 changes: 35 additions & 5 deletions ibis-server/app/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
from enum import Enum
from typing import Annotated, Any, Literal, Union

from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field, SecretStr, model_validator

from app.model.error import ErrorCode, WrenError

manifest_str_field = Field(alias="manifestStr", description="Base64 manifest")
connection_info_field = Field(alias="connectionInfo")
connection_info_field = Field(alias="connectionInfo", default=None)


class BaseConnectionInfo(BaseModel):
Expand All @@ -26,7 +26,17 @@ def to_key_string(self) -> str:
class QueryDTO(BaseModel):
sql: str
manifest_str: str = manifest_str_field
connection_info: dict[str, Any] | ConnectionInfo = connection_info_field
connection_info: dict[str, Any] | ConnectionInfo | None = connection_info_field
connection_file_path: str | None = Field(alias="connectionFilePath", default=None)

@model_validator(mode="after")
def check_connection_source(self):
if self.connection_info is None and self.connection_file_path is None:
raise WrenError(
ErrorCode.INVALID_CONNECTION_INFO,
"Either connectionInfo or connectionFilePath must be provided",
)
return self


class QueryBigQueryDTO(QueryDTO):
Expand Down Expand Up @@ -654,7 +664,17 @@ class GcsFileConnectionInfo(BaseConnectionInfo):
class ValidateDTO(BaseModel):
manifest_str: str = manifest_str_field
parameters: dict
connection_info: dict[str, Any] | ConnectionInfo = connection_info_field
connection_info: dict[str, Any] | ConnectionInfo | None = connection_info_field
connection_file_path: str | None = Field(alias="connectionFilePath", default=None)

@model_validator(mode="after")
def check_connection_source(self):
if self.connection_info is None and self.connection_file_path is None:
raise WrenError(
ErrorCode.INVALID_CONNECTION_INFO,
"Either connectionInfo or connectionFilePath must be provided",
)
return self


class AnalyzeSQLDTO(BaseModel):
Expand All @@ -674,9 +694,19 @@ class DryPlanDTO(BaseModel):

class TranspileDTO(BaseModel):
manifest_str: str = manifest_str_field
connection_info: dict[str, Any] | ConnectionInfo = connection_info_field
connection_info: dict[str, Any] | ConnectionInfo | None = connection_info_field
connection_file_path: str | None = Field(alias="connectionFilePath", default=None)
sql: str

@model_validator(mode="after")
def check_connection_source(self):
if self.connection_info is None and self.connection_file_path is None:
raise WrenError(
ErrorCode.INVALID_CONNECTION_INFO,
"Either connectionInfo or connectionFilePath must be provided",
)
return self


class ConfigModel(BaseModel):
diagnose: bool
Expand Down
17 changes: 15 additions & 2 deletions ibis-server/app/model/metadata/dto.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from enum import Enum
from typing import Any

from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, model_validator

from app.model import ConnectionInfo
from app.model.data_source import DataSource
from app.model.error import ErrorCode, WrenError


class V2MetadataDTO(BaseModel):
Expand All @@ -16,10 +17,22 @@ class FilterInfo(BaseModel):


class MetadataDTO(BaseModel):
connection_info: dict[str, Any] | ConnectionInfo = Field(alias="connectionInfo")
connection_info: dict[str, Any] | ConnectionInfo | None = Field(
alias="connectionInfo", default=None
)
connection_file_path: str | None = Field(alias="connectionFilePath", default=None)
table_limit: int | None = Field(alias="limit", default=None)
filter_info: dict[str, Any] | None = Field(alias="filterInfo", default=None)

@model_validator(mode="after")
def check_connection_source(self):
if self.connection_info is None and self.connection_file_path is None:
raise WrenError(
ErrorCode.INVALID_CONNECTION_INFO,
"Either connectionInfo or connectionFilePath must be provided",
)
return self


class BigQueryFilterInfo(FilterInfo):
projects: list["ProjectDatasets"] | None = None
Expand Down
13 changes: 7 additions & 6 deletions ibis-server/app/routers/v2/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
execute_validate_with_timeout,
get_fallback_message,
pushdown_limit,
resolve_connection_info,
set_attribute,
to_json,
update_response_headers,
Expand Down Expand Up @@ -93,7 +94,7 @@ async def query(
if cache_enable:
span_name += "_cache_enable"
connection_info = data_source.get_connection_info(
dto.connection_info, dict(headers)
resolve_connection_info(dto), dict(headers)
)
# Convert headers to dict for cache manager
headers_dict = dict(headers) if headers else None
Expand Down Expand Up @@ -232,7 +233,7 @@ async def validate(
) as span:
set_attribute(headers, span)
connection_info = data_source.get_connection_info(
dto.connection_info, dict(headers)
resolve_connection_info(dto), dict(headers)
)
validator = Validator(
Connector(data_source, connection_info),
Expand Down Expand Up @@ -273,7 +274,7 @@ async def get_table_list(
) as span:
set_attribute(headers, span)
connection_info = data_source.get_connection_info(
dto.connection_info, dict(headers)
resolve_connection_info(dto), dict(headers)
)
if isinstance(connection_info, BigQueryProjectConnectionInfo):
raise WrenError(
Expand Down Expand Up @@ -302,7 +303,7 @@ async def get_constraints(
) as span:
set_attribute(headers, span)
connection_info = data_source.get_connection_info(
dto.connection_info, dict(headers)
resolve_connection_info(dto), dict(headers)
)
if isinstance(connection_info, BigQueryProjectConnectionInfo):
raise WrenError(
Expand All @@ -324,7 +325,7 @@ async def get_db_version(
headers: Annotated[Headers, Depends(get_wren_headers)] = None,
) -> str:
connection_info = data_source.get_connection_info(
dto.connection_info, dict(headers)
resolve_connection_info(dto), dict(headers)
)
metadata = MetadataFactory.get_metadata(data_source, connection_info)
return await execute_get_version_with_timeout(metadata)
Expand Down Expand Up @@ -398,7 +399,7 @@ async def model_substitute(
) as span:
set_attribute(headers, span)
connection_info = data_source.get_connection_info(
dto.connection_info, dict(headers)
resolve_connection_info(dto), dict(headers)
)
sql = ModelSubstitute(data_source, dto.manifest_str, headers).substitute(
dto.sql, write="trino"
Expand Down
11 changes: 6 additions & 5 deletions ibis-server/app/routers/v3/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
execute_query_with_timeout,
execute_validate_with_timeout,
pushdown_limit,
resolve_connection_info,
safe_strtobool,
set_attribute,
to_json,
Expand Down Expand Up @@ -91,7 +92,7 @@ async def query(
) as span:
set_attribute(headers, span)
connection_info = data_source.get_connection_info(
dto.connection_info, dict(headers)
resolve_connection_info(dto), dict(headers)
)
# Convert headers to dict for cache manager
headers_dict = dict(headers) if headers else None
Expand Down Expand Up @@ -357,7 +358,7 @@ async def validate(
) as span:
set_attribute(headers, span)
connection_info = data_source.get_connection_info(
dto.connection_info, dict(headers)
resolve_connection_info(dto), dict(headers)
)
try:
validator = Validator(
Expand Down Expand Up @@ -485,7 +486,7 @@ async def model_substitute(
) as span:
set_attribute(headers, span)
connection_info = data_source.get_connection_info(
dto.connection_info, dict(headers)
resolve_connection_info(dto), dict(headers)
)
try:
sql = ModelSubstitute(data_source, dto.manifest_str, headers).substitute(
Expand Down Expand Up @@ -569,7 +570,7 @@ async def get_table_list(
) as span:
set_attribute(headers, span)
connection_info = data_source.get_connection_info(
dto.connection_info, dict(headers)
resolve_connection_info(dto), dict(headers)
)
metadata = MetadataFactory.get_metadata(data_source, connection_info)
filter_info = get_filter_info(data_source, dto.filter_info or {})
Expand All @@ -595,7 +596,7 @@ async def get_schema_list(
) as span:
set_attribute(headers, span)
connection_info = data_source.get_connection_info(
dto.connection_info, dict(headers)
resolve_connection_info(dto), dict(headers)
)
metadata = MetadataFactory.get_metadata(data_source, connection_info)
filter_info = get_filter_info(data_source, dto.filter_info or {})
Expand Down
11 changes: 11 additions & 0 deletions ibis-server/app/util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import asyncio
import base64
import json
import pathlib
import time

try:
Expand Down Expand Up @@ -53,6 +55,15 @@ class ClickHouseDbError(Exception):
Wren AI team are appreciate if you can provide the error messages and related logs for us."


def resolve_connection_info(dto) -> dict:
"""Return connection info dict from either a file path or the inline DTO field."""
if getattr(dto, "connection_file_path", None):
path = pathlib.Path(dto.connection_file_path).resolve()
Comment thread
goldmedal marked this conversation as resolved.
Outdated
with open(path) as f:
return json.load(f)
return dto.connection_info
Comment thread
coderabbitai[bot] marked this conversation as resolved.


@tracer.start_as_current_span("base64_to_dict", kind=trace.SpanKind.INTERNAL)
def base64_to_dict(base64_str: str) -> dict:
return orjson.loads(base64.b64decode(base64_str).decode("utf-8"))
Expand Down
27 changes: 23 additions & 4 deletions ibis-server/tests/routers/v2/connector/test_postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,10 +531,9 @@ async def test_query_without_connection_info(client, manifest_str):
)
assert response.status_code == 422
result = response.json()
assert result["detail"][0] is not None
assert result["detail"][0]["type"] == "missing"
assert result["detail"][0]["loc"] == ["body", "connectionInfo"]
assert result["detail"][0]["msg"] == "Field required"
assert result["errorCode"] == "INVALID_CONNECTION_INFO"
assert "connectionInfo" in result["message"]
assert "connectionFilePath" in result["message"]


async def test_query_with_dry_run(client, manifest_str, postgres: PostgresContainer):
Expand Down Expand Up @@ -1089,6 +1088,26 @@ async def test_order_by_nulls_last(client, manifest_str, postgres: PostgresConta
assert result["data"][2][0] == "three"


async def test_connection_info_file(
client, manifest_str, postgres: PostgresContainer, tmp_path
):
conn_file = tmp_path / "connection.json"
conn_file.write_bytes(orjson.dumps(_to_connection_info(postgres)))

response = await client.post(
url=f"{base_url}/query",
json={
"connectionFilePath": str(conn_file),
"manifestStr": manifest_str,
"sql": 'SELECT * FROM "Orders" LIMIT 1',
},
)
assert response.status_code == 200
result = response.json()
assert len(result["columns"]) == len(manifest["models"][0]["columns"])
assert len(result["data"]) == 1


def _to_connection_info(pg: PostgresContainer):
return {
"host": pg.get_container_host_ip(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,25 @@ async def test_validate(client, manifest_str, connection_info):
assert response.status_code == 422


async def test_connection_info_file(client, manifest_str, connection_info, tmp_path):
conn_file = tmp_path / "connection.json"
conn_file.write_bytes(orjson.dumps(connection_info))

# v3 will fail with this manifest and fall back to v2 — connectionFilePath must survive the fallback
response = await client.post(
url=f"{base_url}/query",
json={
"connectionFilePath": str(conn_file),
"manifestStr": manifest_str,
"sql": "SELECT orderkey FROM orders LIMIT 1",
},
)
assert response.status_code == 200
result = response.json()
assert len(result["columns"]) == 1
assert len(result["data"]) == 1


async def test_query_rlac(client, manifest_str, connection_info):
response = await client.post(
url=f"{base_url}/query",
Expand Down
26 changes: 22 additions & 4 deletions ibis-server/tests/routers/v3/connector/postgres/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,10 +427,9 @@ async def test_query_without_connection_info(client, manifest_str):
)
assert response.status_code == 422
result = response.json()
assert result["detail"][0] is not None
assert result["detail"][0]["type"] == "missing"
assert result["detail"][0]["loc"] == ["body", "connectionInfo"]
assert result["detail"][0]["msg"] == "Field required"
assert result["errorCode"] == "INVALID_CONNECTION_INFO"
assert "connectionInfo" in result["message"]
assert "connectionFilePath" in result["message"]


async def test_query_with_dry_run(client, manifest_str, connection_info):
Expand Down Expand Up @@ -1299,3 +1298,22 @@ async def test_to_char_numeric(client, manifest_str, connection_info):
"formatted_number": "string",
"formatted_double": "string",
}


async def test_connection_info_file(client, manifest_str, connection_info, tmp_path):
conn_file = tmp_path / "connection.json"
conn_file.write_bytes(orjson.dumps(connection_info))

response = await client.post(
url=f"{base_url}/query",
json={
"connectionFilePath": str(conn_file),
"manifestStr": manifest_str,
"sql": "SELECT * FROM wren.public.orders LIMIT 1",
},
headers={X_WREN_FALLBACK_DISABLE: "true"},
)
assert response.status_code == 200
result = response.json()
assert len(result["columns"]) == 10
assert len(result["data"]) == 1
14 changes: 13 additions & 1 deletion skills/AUTHORING.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,17 @@ After creating a new skill:

1. Add a section to [SKILLS.md](SKILLS.md) describing the skill, its trigger conditions, and reference files.
2. Add a row to the skills table in [README.md](README.md).
3. Add the skill name and version to [versions.json](versions.json).
4. Add an entry to [index.json](index.json) with `name`, `version`, `description`, `tags`, `dependencies` (if any), and `repository`.
5. Add the skill to the `ALL_SKILLS` array in [install.sh](install.sh).

Both entries should use the same short trigger description.
Both `versions.json` and `index.json` must stay in sync with the `version` field in the skill's `SKILL.md` frontmatter. Run `bash skills/check-versions.sh` to verify parity before merging.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

---

## Releasing a skill update

1. Bump `version` in the skill's `SKILL.md` frontmatter.
2. Update the matching version in `versions.json`.
3. Update the matching version in `index.json`.
4. Run `bash skills/check-versions.sh` — must pass before merging.
Loading
Loading