blob: d378a8d24a2afba549b5ce07051002309ec4316b [file]
# 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.
"""Production evidence contracts for the formal Pipeline runtime."""
from __future__ import annotations
from collections.abc import AsyncIterator, Mapping
from contextlib import asynccontextmanager
from datetime import datetime
from types import SimpleNamespace
from typing import Any
import pytest
from doris_mcp_server.utils.pipeline_runtime import (
DorisPipelineRuntime,
PipelineRuntimeFailure,
)
class _ConnectionManager:
def __init__(
self,
*,
rows: dict[str, list[dict[str, Any]]] | None = None,
failures: dict[str, Exception] | None = None,
) -> None:
self.rows = rows or {}
self.failures = failures or {}
self.calls: list[str] = []
self.params: list[Mapping[str, Any] | tuple[Any, ...] | None] = []
self.context_count = 0
@asynccontextmanager
async def get_connection_context_for_auth_context(
self,
_session_id: str,
_auth_context: Any,
) -> AsyncIterator[Any]:
self.context_count += 1
manager = self
class _Connection:
async def execute(
self,
sql: str,
params: Mapping[str, Any] | tuple[Any, ...] | None = None,
**_kwargs: Any,
) -> SimpleNamespace:
manager.calls.append(sql)
manager.params.append(params)
if sql in manager.failures:
raise manager.failures[sql]
return SimpleNamespace(data=manager.rows.get(sql, []))
yield _Connection()
def _runtime(
*,
rows: dict[str, list[dict[str, Any]]] | None = None,
failures: dict[str, Exception] | None = None,
) -> tuple[DorisPipelineRuntime, _ConnectionManager]:
manager = _ConnectionManager(rows=rows, failures=failures)
return DorisPipelineRuntime(manager), manager # type: ignore[arg-type]
@pytest.mark.asyncio
async def test_ingestion_status_normalizes_real_rows_and_omits_sensitive_fields() -> None:
batch_sql = "SHOW LOAD FROM `analytics` LIMIT 20"
routine_sql = "SHOW ALL ROUTINE LOAD"
runtime, manager = _runtime(
rows={
batch_sql: [
{
"JobId": 101,
"Label": "orders_20260731",
"State": "FINISHED",
"Progress": "100%",
"JobDetails": '{"table_name":"orders"}',
"URL": "http://fe:8030/api/load?token=secret",
"CreateTime": "2026-07-31 01:00:00",
"LoadFinishTime": "2026-07-31 01:01:00",
}
],
routine_sql: [
{
"Id": 202,
"Name": "orders_kafka",
"DbName": "analytics",
"TableName": "orders",
"State": "RUNNING",
"DataSourceType": "KAFKA",
"CurrentTaskNum": 3,
"Lag": '{"0": 4}',
"Progress": '{"0": "17"}',
"ErrorLogUrls": "http://be:8040/error?password=secret",
},
{
"Id": 203,
"Name": "other_db",
"DbName": "other",
"TableName": "orders",
"State": "RUNNING",
},
],
}
)
result = await runtime.get_ingestion_status(
job_types=["batch_load", "routine_load"],
database="analytics",
table="orders",
states=None,
limit=10,
)
assert result["status"] == "success"
assert [item["job_type"] for item in result["data"]["items"]] == [
"batch_load",
"routine_load",
]
assert result["data"]["items"][1]["lag"] == {"0": 4}
serialized = str(result)
assert "ErrorLogUrls" not in serialized
assert "http://fe:8030" not in serialized
assert "password=secret" not in serialized
assert manager.calls == [
batch_sql,
"USE `analytics`",
routine_sql,
]
assert manager.context_count == 2
@pytest.mark.asyncio
async def test_ingestion_status_fails_closed_without_any_evidence_source() -> None:
sql = "SHOW LOAD FROM `analytics` LIMIT 20"
runtime, _ = _runtime(
failures={sql: RuntimeError(1146, "source does not exist")}
)
with pytest.raises(
PipelineRuntimeFailure,
match="ingestion evidence is unavailable",
) as failure:
await runtime.get_ingestion_status(
job_types=["batch_load"],
database="analytics",
table=None,
states=None,
limit=10,
)
assert failure.value.reason_code == "PIPELINE_INGESTION_EVIDENCE_UNAVAILABLE"
@pytest.mark.asyncio
async def test_freshness_prefers_recorded_partition_visibility_time() -> None:
sql = "SHOW PARTITIONS FROM `analytics`.`orders` LIMIT 1000"
now = datetime.now()
runtime, _ = _runtime(
rows={
sql: [
{
"PartitionName": "p20260731",
"VisibleVersion": 12,
"VisibleVersionTime": now,
}
]
}
)
result = await runtime.monitor_data_freshness(
database="analytics",
table="orders",
threshold_seconds=60,
time_column=None,
)
assert result["data"]["status"] == "fresh"
assert result["data"]["method"] == "partition_visible_version"
assert result["data"]["partitions_observed"] == 1
assert result["metadata"]["invented_evidence"] is False
@pytest.mark.asyncio
async def test_freshness_rejects_identifier_injection_before_execution() -> None:
runtime, manager = _runtime()
with pytest.raises(PipelineRuntimeFailure) as failure:
await runtime.monitor_data_freshness(
database="analytics",
table="orders; DROP TABLE users",
threshold_seconds=60,
time_column=None,
)
assert failure.value.reason_code == "PIPELINE_ARGUMENT_INVALID"
assert manager.calls == []
@pytest.mark.asyncio
async def test_materialized_view_status_uses_recorded_jobs_and_refresh_tasks() -> None:
info_sql = (
"SELECT Id, Name, JobName, State, SchemaChangeDetail, RefreshState, "
'RefreshInfo, SyncWithBaseTables FROM mv_infos("database"="analytics") '
"LIMIT 500"
)
job_sql = (
"SELECT Id, Name, MvId, MvName, MvDatabaseName, ExecuteType, "
'RecurringStrategy, Status, CreateTime FROM jobs("type"="mv") '
"WHERE MvDatabaseName = %s LIMIT 500"
)
task_sql = (
"SELECT TaskId, JobId, JobName, MvId, MvName, MvDatabaseName, Status, "
"ErrorMsg, CreateTime, StartTime, FinishTime, DurationMs, RefreshMode, "
"NeedRefreshPartitions, CompletedPartitions, Progress, LastQueryId "
'FROM tasks("type"="mv") WHERE MvDatabaseName = %s '
"ORDER BY CreateTime DESC LIMIT 500"
)
sync_sql = "SHOW ALTER TABLE MATERIALIZED VIEW FROM `analytics`"
runtime, manager = _runtime(
rows={
info_sql: [
{
"Id": 1,
"Name": "mv_orders",
"JobName": "inner_mtmv_1",
"State": "NORMAL",
"RefreshState": "SUCCESS",
"SyncWithBaseTables": "true",
}
],
job_sql: [
{
"Id": 11,
"Name": "inner_mtmv_1",
"MvName": "mv_orders",
"MvDatabaseName": "analytics",
"Status": "RUNNING",
}
],
task_sql: [
{
"TaskId": 21,
"MvName": "mv_orders",
"Status": "SUCCESS",
"DurationMs": 45,
"LastQueryId": "query-1",
}
],
sync_sql: [],
}
)
result = await runtime.get_materialized_view_status(
database="analytics",
view="mv_orders",
states=None,
include_refresh_history=True,
)
item = result["data"]["items"][0]
assert item["view"] == "mv_orders"
assert item["sync_with_base_tables"] is True
assert item["latest_refresh"]["task_id"] == 21
assert item["refresh_history"][0]["duration_ms"] == 45
assert manager.params[1] == ("analytics",)
assert manager.params[2] == ("analytics",)
assert "query_sql" not in str(result).casefold()
@pytest.mark.asyncio
async def test_dependency_graph_uses_only_recorded_edges_without_synthetic_nodes() -> None:
mv_sql = (
'SELECT Name, QuerySql FROM mv_infos("database"="analytics") LIMIT 500'
)
view_sql = (
"SELECT TABLE_NAME, VIEW_DEFINITION FROM information_schema.views "
"WHERE TABLE_SCHEMA = %s LIMIT 500"
)
audit_sql = (
"SELECT stmt, `time` AS observed_at "
"FROM internal.__internal_schema.audit_log "
"WHERE `time` >= DATE_SUB(NOW(), INTERVAL 30 DAY) "
"AND (UPPER(stmt) LIKE '%INSERT%' OR "
"UPPER(stmt) LIKE '%CREATE%VIEW%') "
"ORDER BY `time` DESC LIMIT 500"
)
runtime, _ = _runtime(
rows={
mv_sql: [
{
"Name": "daily_orders",
"QuerySql": "SELECT * FROM raw_orders",
}
],
view_sql: [
{
"TABLE_NAME": "order_view",
"VIEW_DEFINITION": "SELECT * FROM analytics.daily_orders",
}
],
audit_sql: [
{
"stmt": (
"INSERT INTO final_orders "
"SELECT * FROM analytics.order_view"
),
"observed_at": "2026-07-31 02:00:00",
}
],
}
)
result = await runtime.analyze_data_dependencies(
catalog="internal",
database="analytics",
object_name="final_orders",
direction="upstream",
depth=5,
)
assert result["data"]["nodes"] == [
"internal.analytics.daily_orders",
"internal.analytics.final_orders",
"internal.analytics.order_view",
"internal.analytics.raw_orders",
]
assert len(result["data"]["edges"]) == 3
assert "unknown_source" not in str(result)
assert result["metadata"]["invented_evidence"] is False