blob: 3d7adf09dbbfb15832ade69c41622b429f43cd0b [file]
#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
"""
Query executor tests
"""
from datetime import UTC, datetime
from unittest.mock import AsyncMock, Mock, patch
import pytest
from doris_mcp_server.utils.config import DorisConfig
from doris_mcp_server.utils.db import QueryResult
from doris_mcp_server.utils.query_executor import (
CachedQuery,
DorisQueryExecutor,
QueryRequest,
)
from doris_mcp_server.utils.security import AuthContext
class TestDorisQueryExecutor:
"""Doris query executor tests"""
@pytest.fixture
def mock_config(self):
"""Create mock configuration"""
from doris_mcp_server.utils.config import DatabaseConfig, SecurityConfig
config = Mock(spec=DorisConfig)
# Add database config
config.database = Mock(spec=DatabaseConfig)
config.database.host = "localhost"
config.database.port = 9030
config.database.user = "test_user"
config.database.password = "test_password"
config.database.database = "test_db"
config.database.health_check_interval = 60
config.database.max_connections = 20
config.database.connection_timeout = 30
config.database.max_connection_age = 3600
# Add security config
config.security = Mock(spec=SecurityConfig)
config.security.enable_masking = True
config.security.auth_type = "token"
config.security.token_secret = "test_secret"
config.security.token_expiry = 3600
return config
@pytest.fixture
def query_executor(self, mock_config):
"""Create query executor instance"""
# Create a mock connection manager
mock_connection_manager = Mock()
return DorisQueryExecutor(mock_connection_manager, mock_config)
@pytest.mark.asyncio
async def test_background_cleanup_has_explicit_lifecycle(self, query_executor):
assert query_executor._background_tasks == []
await query_executor.start()
cleanup_task = query_executor._background_tasks[0]
await query_executor.start()
assert query_executor._background_tasks == [cleanup_task]
assert cleanup_task.done() is False
await query_executor.close()
assert cleanup_task.done() is True
assert query_executor._background_tasks == []
@pytest.mark.asyncio
async def test_execute_query_success(self, query_executor):
"""Test successful query execution using MCP interface"""
with patch.object(query_executor, 'execute_sql_for_mcp') as mock_execute:
mock_execute.return_value = {
"success": True,
"data": [
{"id": 1, "name": "张三", "email": "zhangsan@example.com"},
{"id": 2, "name": "李四", "email": "lisi@example.com"}
],
"row_count": 2,
"execution_time": 0.15,
"columns": ["id", "name", "email"]
}
sql = "SELECT id, name, email FROM users LIMIT 2"
result = await query_executor.execute_sql_for_mcp(sql)
# Verify results
assert result["success"] is True
assert result["row_count"] == 2
assert len(result["data"]) == 2
assert result["data"][0]["id"] == 1
assert result["data"][0]["name"] == "张三"
assert result["data"][1]["email"] == "lisi@example.com"
@pytest.mark.asyncio
async def test_execute_query_with_parameters(self, query_executor):
"""Test query execution with parameters"""
with patch.object(query_executor, 'execute_sql_for_mcp') as mock_execute:
mock_execute.return_value = {
"success": True,
"data": [{"id": 1, "name": "张三"}],
"row_count": 1,
"execution_time": 0.1
}
sql = "SELECT id, name FROM users WHERE department = 'sales'"
result = await query_executor.execute_sql_for_mcp(sql)
# Verify results
assert result["success"] is True
assert result["row_count"] == 1
assert len(result["data"]) == 1
@pytest.mark.asyncio
async def test_execute_query_connection_error(self, query_executor):
"""Test query execution with connection error"""
with patch.object(query_executor, 'execute_sql_for_mcp') as mock_execute:
mock_execute.return_value = {
"success": False,
"error": "Connection failed",
"data": None
}
sql = "SELECT * FROM users"
result = await query_executor.execute_sql_for_mcp(sql)
assert result["success"] is False
assert "Connection failed" in result["error"]
@pytest.mark.asyncio
async def test_execute_query_sql_error(self, query_executor):
"""Test query execution with SQL error"""
with patch.object(query_executor, 'execute_sql_for_mcp') as mock_execute:
mock_execute.return_value = {
"success": False,
"error": "SQL syntax error",
"data": None
}
sql = "SELECT * FROM non_existent_table"
result = await query_executor.execute_sql_for_mcp(sql)
assert result["success"] is False
assert "SQL syntax error" in result["error"]
@pytest.mark.asyncio
async def test_execute_query_empty_result(self, query_executor):
"""Test query execution with empty result"""
with patch.object(query_executor, 'execute_sql_for_mcp') as mock_execute:
mock_execute.return_value = {
"success": True,
"data": [],
"row_count": 0,
"execution_time": 0.05
}
sql = "SELECT * FROM users WHERE id = 999"
result = await query_executor.execute_sql_for_mcp(sql)
assert result["success"] is True
assert result["data"] == []
assert result["row_count"] == 0
@pytest.mark.asyncio
async def test_execute_query_max_rows_limit(self, query_executor):
"""Test query execution with max rows limit"""
with patch.object(query_executor, 'execute_sql_for_mcp') as mock_execute:
# Mock large result set limited to 100 rows
limited_result = [{"id": i, "name": f"user_{i}"} for i in range(100)]
mock_execute.return_value = {
"success": True,
"data": limited_result,
"row_count": 100,
"execution_time": 0.2
}
sql = "SELECT id, name FROM users"
result = await query_executor.execute_sql_for_mcp(sql, limit=100)
# Should be limited to max_rows
assert result["success"] is True
assert len(result["data"]) == 100
@pytest.mark.asyncio
async def test_execute_sql_for_mcp_interface(self, query_executor):
"""Test the MCP interface method directly"""
with patch.object(query_executor.connection_manager, 'get_connection') as mock_get_conn:
# Mock connection and result
mock_connection = AsyncMock()
mock_connection.execute.return_value = Mock(
data=[{"id": 1, "name": "张三"}],
row_count=1,
execution_time=0.1,
metadata={}
)
mock_get_conn.return_value = mock_connection
sql = "SELECT id, name FROM users LIMIT 1"
result = await query_executor.execute_sql_for_mcp(sql)
# Should return success format
assert "success" in result
if result["success"]:
assert "data" in result
assert "row_count" in result
@pytest.mark.asyncio
async def test_execute_multi_sql_statements(self, query_executor):
"""Test execution of multiple SQL statements"""
from doris_mcp_server.utils.query_executor import QueryResult
# Disable security check for this test
query_executor.connection_manager.config.security.enable_security_check = False
with patch.object(query_executor, 'execute_query') as mock_execute:
# Mock results for three SQL statements
mock_execute.side_effect = [
QueryResult(
data=[{"id": 1, "name": "张三"}],
row_count=1,
execution_time=0.1,
sql="SELECT id, name FROM users WHERE id = 1",
metadata={"columns": ["id", "name"]}
),
QueryResult(
data=[{"id": 2, "name": "李四"}],
row_count=1,
execution_time=0.12,
sql="SELECT id, name FROM users WHERE id = 2",
metadata={"columns": ["id", "name"]}
),
QueryResult(
data=[{"count": 100}],
row_count=1,
execution_time=0.08,
sql="SELECT COUNT(*) as count FROM users",
metadata={"columns": ["count"]}
)
]
# Execute multiple SQL statements separated by semicolons
multi_sql = """
SELECT id, name FROM users WHERE id = 1;
SELECT id, name FROM users WHERE id = 2;
SELECT COUNT(*) as count FROM users;
"""
result = await query_executor.execute_sql_for_mcp(multi_sql)
# Verify the result structure for multiple statements
assert result["success"] is True
assert result["multiple_results"] is True
assert "results" in result
assert len(result["results"]) == 3
# Verify first query result
assert result["results"][0]["data"] == [{"id": 1, "name": "张三"}]
assert result["results"][0]["row_count"] == 1
assert result["results"][0]["metadata"]["columns"] == ["id", "name"]
assert result["results"][0]["metadata"]["query"] == "SELECT id, name FROM users WHERE id = 1"
# Verify second query result
assert result["results"][1]["data"] == [{"id": 2, "name": "李四"}]
assert result["results"][1]["row_count"] == 1
assert result["results"][1]["metadata"]["columns"] == ["id", "name"]
assert result["results"][1]["metadata"]["query"] == "SELECT id, name FROM users WHERE id = 2"
# Verify third query result
assert result["results"][2]["data"] == [{"count": 100}]
assert result["results"][2]["row_count"] == 1
assert result["results"][2]["metadata"]["columns"] == ["count"]
assert result["results"][2]["metadata"]["query"] == "SELECT COUNT(*) as count FROM users"
# Verify execute_query was called three times
assert mock_execute.call_count == 3
@pytest.mark.asyncio
async def test_doris_oauth_cache_enabled_query_forces_no_cache(self, query_executor):
query_executor.query_cache.get = AsyncMock()
query_executor.query_cache.set = AsyncMock()
query_executor.query_optimizer.optimize_query = AsyncMock(return_value="SELECT 1")
query_executor.connection_manager.execute_query = AsyncMock(
return_value=QueryResult(
data=[{"one": 1}],
metadata={},
execution_time=0.01,
row_count=1,
sql="SELECT 1",
)
)
auth_context = AuthContext(
auth_method="doris_oauth",
roles=["doris_oauth_user"],
doris_user="alice",
oauth_scopes=["tool:call:exec_query"],
pool_key="doris_user:alice",
)
result = await query_executor.execute_query(
QueryRequest(
sql="SELECT 1",
session_id="s1",
user_id="alice",
cache_enabled=True,
),
auth_context,
)
assert result.row_count == 1
query_executor.query_cache.get.assert_not_awaited()
query_executor.query_cache.set.assert_not_awaited()
query_executor.connection_manager.execute_query.assert_awaited_once()
@pytest.mark.asyncio
async def test_static_or_global_cache_enabled_query_still_uses_query_cache(self, query_executor):
cached_result = QueryResult(
data=[{"cached": True}],
metadata={},
execution_time=0.01,
row_count=1,
sql="SELECT 1",
)
query_executor.query_cache.get = AsyncMock(
return_value=CachedQuery(
result=cached_result,
created_at=datetime.now(UTC),
ttl=300,
)
)
query_executor.query_cache.set = AsyncMock()
query_executor.connection_manager.execute_query = AsyncMock()
result = await query_executor.execute_query(
QueryRequest(
sql="SELECT 1",
session_id="s1",
user_id="legacy",
cache_enabled=True,
),
AuthContext(auth_method="token"),
)
assert result is cached_result
query_executor.query_cache.get.assert_awaited_once()
query_executor.query_cache.set.assert_not_awaited()
query_executor.connection_manager.execute_query.assert_not_awaited()