arcade-mcp/toolkits/clickhouse/tests/test_clickhouse.py
Evan Tahler a85fa76997
Clickhouse Toolkit (#527)
Generated from the postgres toolkit
2025-08-05 14:05:09 -07:00

200 lines
5.8 KiB
Python

import os
from os import environ
import pytest
import pytest_asyncio
from arcade_clickhouse.tools.clickhouse import (
DatabaseEngine,
discover_schemas,
discover_tables,
execute_select_query,
get_table_schema,
)
from arcade_tdk import ToolContext, ToolSecretItem
from arcade_tdk.errors import RetryableToolError
CLICKHOUSE_DATABASE_CONNECTION_STRING = (
environ.get("TEST_CLICKHOUSE_DATABASE_CONNECTION_STRING")
or "clickhouse+native://localhost:9000/default"
)
@pytest.fixture
def mock_context():
context = ToolContext()
context.secrets = []
context.secrets.append(
ToolSecretItem(
key="CLICKHOUSE_DATABASE_CONNECTION_STRING", value=CLICKHOUSE_DATABASE_CONNECTION_STRING
)
)
return context
# before the tests, restore the database from the dump
@pytest_asyncio.fixture(autouse=True)
async def restore_database():
import clickhouse_connect
# Create client for database setup
client = clickhouse_connect.get_client(host="localhost", port=8123)
# Clear existing tables first to avoid duplicates
client.command("DROP TABLE IF EXISTS default.messages")
client.command("DROP TABLE IF EXISTS default.users")
# Read and execute the dump file
with open(f"{os.path.dirname(__file__)}/dump.sql") as f:
queries = f.read().split(";")
for query in queries:
if query.strip():
client.command(query)
client.close()
@pytest_asyncio.fixture(autouse=True)
async def cleanup_engines():
"""Clean up database engines after each test to prevent connection leaks."""
yield
# Clean up all cached engines after each test
await DatabaseEngine.cleanup()
@pytest.mark.asyncio
async def test_discover_schemas(mock_context) -> None:
assert await discover_schemas(mock_context) == ["default"]
@pytest.mark.asyncio
async def test_discover_tables(mock_context) -> None:
tables = await discover_tables(mock_context)
assert sorted(tables) == ["messages", "users"]
@pytest.mark.asyncio
async def test_get_table_schema(mock_context) -> None:
users_schema = await get_table_schema(mock_context, "default", "users")
expected_users = [
"id: UInt32 (PRIMARY KEY)",
"name: String",
"email: String",
"password_hash: String",
"created_at: DateTime (PRIMARY KEY)",
"updated_at: DateTime",
"status: String",
]
assert users_schema == expected_users
messages_schema = await get_table_schema(mock_context, "default", "messages")
expected_messages = [
"id: UInt32 (PRIMARY KEY)",
"body: String",
"user_id: UInt32",
"created_at: DateTime (PRIMARY KEY)",
"updated_at: DateTime",
]
assert messages_schema == expected_messages
@pytest.mark.asyncio
async def test_execute_select_query(mock_context) -> None:
# Test specific user query with limit
result1 = await execute_select_query(
mock_context,
select_clause="id, name, email",
from_clause="users",
where_clause="id = 1",
limit=1,
)
assert result1 == ["(1, 'Alice', 'alice@example.com')"]
# Test query with offset
result2 = await execute_select_query(
mock_context,
select_clause="id, name, email",
from_clause="users",
order_by_clause="id",
limit=1,
offset=1,
)
assert result2 == ["(2, 'Bob', 'bob@example.com')"]
@pytest.mark.asyncio
async def test_execute_select_query_with_keywords(mock_context) -> None:
result = await execute_select_query(
mock_context,
select_clause="SELECT id, name, email",
from_clause="FROM users",
limit=1,
)
assert result == ["(1, 'Alice', 'alice@example.com')"]
@pytest.mark.asyncio
async def test_execute_select_query_with_join(mock_context) -> None:
result = await execute_select_query(
mock_context,
select_clause="u.id, u.name, u.email, m.id, m.body",
from_clause="users u",
join_clause="messages m ON u.id = m.user_id",
limit=1,
)
assert result == ["(1, 'Alice', 'alice@example.com', 1, 'Hello everyone!')"]
@pytest.mark.asyncio
async def test_execute_select_query_with_group_by(mock_context) -> None:
result = await execute_select_query(
mock_context,
select_clause="u.name, COUNT(m.id) AS message_count",
from_clause="messages m",
join_clause="users u ON m.user_id = u.id",
group_by_clause="u.name",
order_by_clause="message_count DESC",
limit=2,
)
assert result == [
"('Evan', 13)",
"('Alice', 3)",
]
@pytest.mark.asyncio
async def test_execute_select_query_with_no_results(mock_context) -> None:
# does not raise an error
assert (
await execute_select_query(
mock_context,
select_clause="id, name, email",
from_clause="users",
where_clause="id = 9999999999",
)
== []
)
@pytest.mark.asyncio
async def test_execute_select_query_with_problem(mock_context) -> None:
# 'foo' is not a valid id
with pytest.raises(RetryableToolError) as e:
await execute_select_query(
mock_context,
select_clause="*",
from_clause="users",
where_clause="id = 'foo'",
)
assert "Do not use * in the select clause" in str(e.value)
@pytest.mark.asyncio
async def test_execute_select_query_rejects_non_select(mock_context) -> None:
with pytest.raises(RetryableToolError) as e:
await execute_select_query(
mock_context,
select_clause="INSERT INTO users (name, email, password_hash) VALUES ('Luigi', 'luigi@example.com', 'password')",
from_clause="users",
)
assert "Only SELECT queries are allowed" in str(e.value)