Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
83 changes: 83 additions & 0 deletions .github/workflows/wren-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,86 @@ jobs:
uv sync --extra ${{ matrix.extra }} --extra dev --find-links ../wren-core-py/target/wheels/
- name: Run ${{ matrix.datasource }} tests
run: uv run pytest ${{ matrix.test_file }} -v -m ${{ matrix.marker }}

test-ui:
name: ui tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: wren
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
./wren-core-py/target/
key: ${{ runner.os }}-cargo-wren-${{ hashFiles('**/Cargo.lock') }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Build wren-core-py wheel
working-directory: wren-core-py
run: |
pipx install poetry
poetry install --no-root
poetry run maturin build
- name: Install dependencies
run: |
uv lock --find-links ../wren-core-py/target/wheels/ --upgrade-package wren-core-py
uv sync --extra dev --extra ui --find-links ../wren-core-py/target/wheels/
- name: Run UI tests
run: uv run pytest tests/test_profile_web.py tests/test_field_registry.py -v

test-memory:
name: memory tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: wren
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
./wren-core-py/target/
key: ${{ runner.os }}-cargo-wren-${{ hashFiles('**/Cargo.lock') }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Build wren-core-py wheel
working-directory: wren-core-py
run: |
pipx install poetry
poetry install --no-root
poetry run maturin build
- name: Install dependencies
run: |
uv lock --find-links ../wren-core-py/target/wheels/ --upgrade-package wren-core-py
uv sync --extra dev --extra memory --find-links ../wren-core-py/target/wheels/
- name: Cache sentence-transformers model
uses: actions/cache@v4
with:
path: ~/.cache/huggingface
key: hf-paraphrase-MiniLM-L3-v2
- name: Run memory tests
env:
WREN_EMBEDDING_MODEL: paraphrase-MiniLM-L3-v2
run: uv run pytest tests/unit/test_memory.py -v
299 changes: 183 additions & 116 deletions mcp-server/app/web.py

Large diffs are not rendered by default.

61 changes: 57 additions & 4 deletions wren/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ pip install wren-engine[spark] # Spark
pip install wren-engine[athena] # Athena
pip install wren-engine[oracle] # Oracle
pip install 'wren-engine[memory]' # Schema & query memory (LanceDB)
pip install 'wren-engine[all]' # All connectors + memory
pip install 'wren-engine[ui]' # Browser-based profile form (starlette + uvicorn)
pip install 'wren-engine[all]' # All connectors + memory + ui
```

Requires Python 3.11+.
Expand Down Expand Up @@ -54,7 +55,20 @@ Requires Python 3.11+.
}
```

**2. Create `~/.wren/connection_info.json`** — your connection:
**2. Configure a connection profile** — pick one of three ways:

```bash
# Browser form (recommended, requires wren-engine[ui])
wren profile add my-db --ui

# Interactive terminal prompts
wren profile add my-db --interactive

# Import from an existing connection file
wren profile add my-db --from-file connection_info.json
```

Or create `~/.wren/connection_info.json` manually:

```json
{
Expand All @@ -73,7 +87,7 @@ Requires Python 3.11+.
wren --sql 'SELECT order_id FROM "orders" LIMIT 10'
```

For the full CLI reference and per-datasource `connection_info.json` formats, see [`docs/cli.md`](docs/cli.md) and [`docs/connections.md`](docs/connections.md).
For the full CLI reference and per-datasource connection field reference, see [`docs/cli.md`](docs/cli.md) and [`docs/connections.md`](docs/connections.md).

**4. Index schema for semantic search** (optional, requires `wren-engine[memory]`):

Expand All @@ -86,6 +100,38 @@ wren memory recall -q "best customers" # retrieve similar past queries

---

## Connection profiles

Profiles let you store named connection configurations in `~/.wren/profiles.yml` and switch between them easily — useful when working across multiple databases or environments.

```bash
# Add a profile (browser form, interactive prompts, or file import)
wren profile add prod --ui # opens http://localhost:<port>
wren profile add staging --interactive # terminal prompts
wren profile add local --from-file conn.json # import existing file

# List and switch profiles
wren profile list # * marks the active profile
wren profile switch prod

# Inspect a profile (sensitive fields masked)
wren profile debug prod

# Remove a profile
wren profile rm old-profile --force
```

The `--ui` flag opens a browser-based form that auto-derives fields from each datasource's schema — including file upload for BigQuery credentials, variant selection for Databricks/Redshift, and sensible defaults for all 20+ supported sources. Requires `pip install 'wren-engine[ui]'`.

Once a profile is active, `wren` uses it automatically:

```bash
wren profile switch prod
wren --sql 'SELECT COUNT(*) FROM "orders"' # connects using prod profile
```

---

## Python SDK

```python
Expand All @@ -112,12 +158,19 @@ just format # Auto-fix

| Command | What it runs | Docker needed |
|---------|-------------|---------------|
| `just test-unit` | Unit tests | No |
| `just test-unit` | Unit tests (engine, CTE rewriter, field registry, profiles) | No |
| `just test-duckdb` | DuckDB connector tests | No |
| `just test-postgres` | PostgreSQL connector tests | Yes |
| `just test-mysql` | MySQL connector tests | Yes |
| `just test` | All tests | Yes |

Profile web tests (`test_profile_web.py`) require `wren-engine[ui]`:

```bash
uv sync --extra dev --extra ui --find-links ../wren-core-py/target/wheels/
uv run pytest tests/test_profile_web.py -v
```

## Publishing

```bash
Expand Down
5 changes: 4 additions & 1 deletion wren/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,16 @@ spark = ["pyspark>=3.5"]
athena = ["ibis-framework[athena]"]
oracle = ["oracledb>=2"]
memory = ["lancedb>=0.6", "sentence-transformers>=2.2"]
ui = ["starlette>=0.37", "uvicorn>=0.29", "jinja2>=3.1", "python-multipart>=0.0.9"]
all = [
"wren-engine[postgres,mysql,bigquery,snowflake,clickhouse,trino,mssql,databricks,redshift,athena,oracle,spark,memory]",
"wren-engine[postgres,mysql,bigquery,snowflake,clickhouse,trino,mssql,databricks,redshift,athena,oracle,spark,memory,ui]",
]
dev = [
"pytest>=8",
"ruff>=0.4",
"orjson>=3",
"testcontainers[postgres,mysql]>=4",
"httpx>=0.27",
]

[project.scripts]
Expand All @@ -76,6 +78,7 @@ path = "src/wren/__init__.py"

[tool.hatch.build.targets.wheel]
packages = ["src/wren"]
artifacts = ["src/wren/templates/*.html"]

[tool.ruff]
line-length = 88
Expand Down
14 changes: 6 additions & 8 deletions wren/src/wren/connector/databricks.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,25 @@ def __init__(self, connection_info: DatabricksConnectionUnion):

if isinstance(connection_info, DatabricksTokenConnectionInfo):
self.connection = dbsql.connect(
server_hostname=connection_info.server_hostname.get_secret_value(),
http_path=connection_info.http_path.get_secret_value(),
server_hostname=connection_info.server_hostname,
http_path=connection_info.http_path,
access_token=connection_info.access_token.get_secret_value(),
)
elif isinstance(connection_info, DatabricksServicePrincipalConnectionInfo):
kwargs = {
"host": connection_info.server_hostname.get_secret_value(),
"host": connection_info.server_hostname,
"client_id": connection_info.client_id.get_secret_value(),
"client_secret": connection_info.client_secret.get_secret_value(),
}
if connection_info.azure_tenant_id is not None:
kwargs["azure_tenant_id"] = (
connection_info.azure_tenant_id.get_secret_value()
)
kwargs["azure_tenant_id"] = connection_info.azure_tenant_id

def credential_provider():
return oauth_service_principal(DbConfig(**kwargs))

self.connection = dbsql.connect(
server_hostname=connection_info.server_hostname.get_secret_value(),
http_path=connection_info.http_path.get_secret_value(),
server_hostname=connection_info.server_hostname,
http_path=connection_info.http_path,
credentials_provider=credential_provider,
)

Expand Down
10 changes: 4 additions & 6 deletions wren/src/wren/connector/duckdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def _init_duckdb_s3(connection, info: S3FileConnectionInfo):
TYPE S3,
KEY_ID '{_escape_sql(info.access_key.get_secret_value())}',
SECRET '{_escape_sql(info.secret_key.get_secret_value())}',
REGION '{_escape_sql(info.region.get_secret_value())}'
REGION '{_escape_sql(info.region)}'
)""")


Expand All @@ -35,7 +35,7 @@ def _init_duckdb_minio(connection, info: MinioFileConnectionInfo):
SECRET '{_escape_sql(info.secret_key.get_secret_value())}',
REGION 'ap-northeast-1'
)""")
connection.execute("SET s3_endpoint=?", [info.endpoint.get_secret_value()])
connection.execute("SET s3_endpoint=?", [info.endpoint])
connection.execute("SET s3_url_style='path'")
connection.execute("SET s3_use_ssl=?", [info.ssl_enabled])

Expand Down Expand Up @@ -98,16 +98,14 @@ def _attach_database(self, connection_info) -> None:
)

def _list_duckdb_files(self, connection_info) -> list[str]:
op = opendal.Operator("fs", root=connection_info.url.get_secret_value())
op = opendal.Operator("fs", root=connection_info.url)
files = []
try:
for file in op.list("/"):
if file.path != "/":
stat = op.stat(file.path)
if not stat.mode.is_dir() and file.path.endswith(".duckdb"):
files.append(
f"{connection_info.url.get_secret_value()}/{file.path}"
)
files.append(f"{connection_info.url}/{file.path}")
except Exception as e:
raise WrenError(
ErrorCode.GENERIC_USER_ERROR, f"Failed to list files: {e!s}"
Expand Down
10 changes: 5 additions & 5 deletions wren/src/wren/connector/oracle.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def _make_oracle_connection(connection_info):
)
if hasattr(connection_info, "dsn") and connection_info.dsn:
return oracledb.connect(
user=connection_info.user.get_secret_value(),
user=connection_info.user,
password=(
connection_info.password.get_secret_value()
if connection_info.password
Expand All @@ -113,15 +113,15 @@ def _make_oracle_connection(connection_info):
dsn=connection_info.dsn.get_secret_value(),
)
return oracledb.connect(
user=connection_info.user.get_secret_value(),
user=connection_info.user,
password=(
connection_info.password.get_secret_value()
if connection_info.password
else None
),
host=connection_info.host.get_secret_value(),
port=int(connection_info.port.get_secret_value()),
service_name=connection_info.database.get_secret_value(),
host=connection_info.host,
port=int(connection_info.port),
service_name=connection_info.database,
)


Expand Down
16 changes: 8 additions & 8 deletions wren/src/wren/connector/redshift.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ def __init__(self, connection_info: RedshiftConnectionUnion):
if isinstance(connection_info, RedshiftIAMConnectionInfo):
self.connection = redshift_connector.connect(
iam=True,
cluster_identifier=connection_info.cluster_identifier.get_secret_value(),
database=connection_info.database.get_secret_value(),
db_user=connection_info.user.get_secret_value(),
cluster_identifier=connection_info.cluster_identifier,
database=connection_info.database,
db_user=connection_info.user,
access_key_id=connection_info.access_key_id.get_secret_value(),
secret_access_key=connection_info.access_key_secret.get_secret_value(),
region=connection_info.region.get_secret_value(),
region=connection_info.region,
)
elif isinstance(connection_info, RedshiftConnectionInfo):
self.connection = redshift_connector.connect(
host=connection_info.host.get_secret_value(),
port=int(connection_info.port.get_secret_value()),
database=connection_info.database.get_secret_value(),
user=connection_info.user.get_secret_value(),
host=connection_info.host,
port=int(connection_info.port),
database=connection_info.database,
user=connection_info.user,
password=connection_info.password.get_secret_value(),
)
else:
Expand Down
4 changes: 2 additions & 2 deletions wren/src/wren/connector/spark.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ def __init__(self, connection_info: SparkConnectionInfo):
def _create_session(self):
from pyspark.sql import SparkSession # noqa: PLC0415

host = self.connection_info.host.get_secret_value()
port = self.connection_info.port.get_secret_value()
host = self.connection_info.host
port = self.connection_info.port
return (
SparkSession.builder.remote(f"sc://{host}:{port}")
.appName("wren")
Expand Down
Loading
Loading