This is an automated email from the ASF dual-hosted git repository.
FreeOnePlus pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-mcp-server.git
The following commit(s) were added to refs/heads/master by this push:
new 966df49 feat: add validated hierarchical domain catalog (#167)
966df49 is described below
commit 966df494e859717a7eeae54b4a64792086335401
Author: Yijia Su <[email protected]>
AuthorDate: Fri Jul 31 02:57:32 2026 +0800
feat: add validated hierarchical domain catalog (#167)
---
doris_mcp_server/tools/domain_catalog.py | 1971 ++++++++++++++++++++++++++++++
doris_mcp_server/tools/tool_catalog.py | 6 +-
test/tools/test_domain_catalog.py | 621 ++++++++++
3 files changed, 2597 insertions(+), 1 deletion(-)
diff --git a/doris_mcp_server/tools/domain_catalog.py
b/doris_mcp_server/tools/domain_catalog.py
new file mode 100644
index 0000000..7c31730
--- /dev/null
+++ b/doris_mcp_server/tools/domain_catalog.py
@@ -0,0 +1,1971 @@
+# 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.
+
+"""Authoritative hierarchical domain catalog for the Doris MCP 1.0 surface."""
+
+from __future__ import annotations
+
+from enum import StrEnum
+from typing import Annotated, Any, Self
+
+from pydantic import Field, model_validator
+
+from .domain_models import (
+ ChildToolDefinition,
+ CompositePlan,
+ CompositeStep,
+ ContractModel,
+ DomainDefinition,
+ Identifier,
+ NonEmptyText,
+ ToolContractAnnotations,
+)
+from .doris_feature_matrix import (
+ DORIS_FEATURE_MATRIX,
+ EXPECTED_DOMAIN_CHILDREN,
+)
+from .tool_registry import (
+ ToolDefinitionRegistry,
+ ToolHandlerOwner,
+ ToolRegistryError,
+)
+
+CURRENT_FLAT_TOOL_NAMES = (
+ "exec_query",
+ "get_table_schema",
+ "get_db_table_list",
+ "get_db_list",
+ "get_table_comment",
+ "get_table_column_comments",
+ "get_table_indexes",
+ "get_recent_audit_logs",
+ "get_catalog_list",
+ "get_sql_explain",
+ "get_sql_profile",
+ "get_table_data_size",
+ "get_monitoring_metrics",
+ "get_memory_stats",
+ "get_table_basic_info",
+ "analyze_columns",
+ "analyze_table_storage",
+ "trace_column_lineage",
+ "monitor_data_freshness",
+ "analyze_data_access_patterns",
+ "analyze_data_flow_dependencies",
+ "analyze_slow_queries_topn",
+ "analyze_resource_growth_curves",
+ "exec_adbc_query",
+ "get_adbc_connection_info",
+)
+
+_READ_ONLY_ANNOTATIONS = ToolContractAnnotations(
+ read_only=True,
+ idempotent=True,
+ destructive=False,
+ open_world=False,
+ requires_confirmation=False,
+)
+
+
+class DomainCatalogError(ValueError):
+ """Raised when the hierarchical catalog or migration map is invalid."""
+
+
+class LegacyMigrationMode(StrEnum):
+ """How one pre-1.0 flat tool contributes to a formal child capability."""
+
+ ATOMIC = "atomic"
+ ADAPTED = "adapted"
+ COMPOSITE_SECTION = "composite_section"
+
+
+class LegacyToolMigration(ContractModel):
+ """Auditable migration from one current flat tool to one formal child."""
+
+ legacy_tool_name: Identifier
+ legacy_handler_name: NonEmptyText
+ domain: Identifier
+ child_name: Identifier
+ mode: LegacyMigrationMode
+ adapter_name: NonEmptyText | None = None
+ target_section: Identifier | None = None
+
+ @property
+ def feature_id(self) -> str:
+ return f"{self.domain}.{self.child_name}"
+
+ @model_validator(mode="after")
+ def _validate_mode(self) -> Self:
+ if self.mode is LegacyMigrationMode.COMPOSITE_SECTION:
+ if self.target_section is None:
+ raise ValueError(
+ "composite-section migrations require target_section"
+ )
+ elif self.target_section is not None:
+ raise ValueError(
+ "target_section is only valid for composite-section migrations"
+ )
+ if self.mode is LegacyMigrationMode.ADAPTED and self.adapter_name is
None:
+ raise ValueError("adapted migrations require adapter_name")
+ if self.mode is not LegacyMigrationMode.ADAPTED and self.adapter_name
is not None:
+ raise ValueError("adapter_name is only valid for adapted
migrations")
+ return self
+
+
+class DomainCatalogDomainSummary(ContractModel):
+ """Stable summary for one top-level domain."""
+
+ name: Identifier
+ child_count: Annotated[int, Field(ge=1)]
+ child_names: tuple[Identifier, ...]
+
+
+class DomainCatalogSummary(ContractModel):
+ """Machine-readable summary used by startup and acceptance checks."""
+
+ domain_count: Annotated[int, Field(ge=1)]
+ child_count: Annotated[int, Field(ge=1)]
+ migrated_flat_tool_count: Annotated[int, Field(ge=0)]
+ domains: tuple[DomainCatalogDomainSummary, ...]
+
+
+class DorisDomainCatalog(ContractModel):
+ """Single validated catalog for domains, children, and legacy migration."""
+
+ domains: Annotated[tuple[DomainDefinition, ...], Field(min_length=1)]
+ legacy_migrations: tuple[LegacyToolMigration, ...]
+
+ @model_validator(mode="after")
+ def _validate_model(self) -> Self:
+ self.validate_integrity()
+ return self
+
+ def validate_integrity(self) -> None:
+ """Reject catalog drift before any MCP tool can be exposed."""
+ for domain in self.domains:
+ if len(domain.children) > 12:
+ raise DomainCatalogError(
+ f"domain {domain.name!r} exceeds the 12-child limit"
+ )
+
+ expected = {
+ domain: tuple(children)
+ for domain, children in EXPECTED_DOMAIN_CHILDREN.items()
+ }
+ actual = {
+ domain.name: tuple(child.name for child in domain.children)
+ for domain in self.domains
+ }
+ if actual != expected:
+ raise DomainCatalogError(
+ "domain catalog must contain the exact ordered "
+ "8-domain/47-child contract"
+ )
+
+ domain_names = tuple(domain.name for domain in self.domains)
+ _require_unique(domain_names, "domain names")
+ child_feature_ids = tuple(
+ f"{domain.name}.{child.name}"
+ for domain in self.domains
+ for child in domain.children
+ )
+ _require_unique(child_feature_ids, "formal child feature IDs")
+ handler_names = tuple(
+ child.handler_name
+ for domain in self.domains
+ for child in domain.children
+ )
+ _require_unique(handler_names, "formal child handler bindings")
+
+ expected_contracts = {
+ feature.feature_id: feature.support_contract
+ for feature in DORIS_FEATURE_MATRIX.features
+ }
+ for domain in self.domains:
+ if not domain.annotations.read_only:
+ raise DomainCatalogError(
+ f"read-only domain {domain.name!r} is not marked read-only"
+ )
+ for child in domain.children:
+ feature_id = f"{domain.name}.{child.name}"
+ if child.support_contract != expected_contracts[feature_id]:
+ raise DomainCatalogError(
+ f"{feature_id} does not use the feature-matrix
contract"
+ )
+ if (
+ not child.annotations.read_only
+ or child.annotations.destructive
+ or child.annotations.requires_confirmation
+ ):
+ raise DomainCatalogError(
+ f"{feature_id} violates the read-only risk contract"
+ )
+ expected_handler = f"child:{domain.name}:{child.name}"
+ if child.handler_name != expected_handler:
+ raise DomainCatalogError(
+ f"{feature_id} has non-canonical handler binding"
+ )
+ expected_policy = f"child:call:{domain.name}:{child.name}"
+ if child.authorization_policy != expected_policy:
+ raise DomainCatalogError(
+ f"{feature_id} has non-canonical authorization policy"
+ )
+
+ migration_names = tuple(
+ migration.legacy_tool_name for migration in self.legacy_migrations
+ )
+ if migration_names != CURRENT_FLAT_TOOL_NAMES:
+ raise DomainCatalogError(
+ "legacy migration map must cover the exact ordered 25-tool
baseline"
+ )
+ _require_unique(migration_names, "legacy migration tool names")
+
+ known_features = set(child_feature_ids)
+ for migration in self.legacy_migrations:
+ if migration.feature_id not in known_features:
+ raise DomainCatalogError(
+ f"legacy tool {migration.legacy_tool_name!r} targets
unknown "
+ f"child {migration.feature_id!r}"
+ )
+ target = self.resolve_child(
+ migration.domain,
+ migration.child_name,
+ )
+ if (
+ migration.mode is LegacyMigrationMode.COMPOSITE_SECTION
+ and target.composite_plan is None
+ ):
+ raise DomainCatalogError(
+ f"legacy tool {migration.legacy_tool_name!r} targets a "
+ "non-composite child"
+ )
+
+ table_context_migrations = tuple(
+ migration
+ for migration in self.legacy_migrations
+ if migration.feature_id == "doris_catalog.get_table_context"
+ )
+ expected_sections = (
+ "schema",
+ "comments",
+ "comments",
+ "indexes",
+ "basic",
+ )
+ if tuple(
+ migration.target_section for migration in table_context_migrations
+ ) != expected_sections:
+ raise DomainCatalogError(
+ "get_table_context must merge the exact five legacy handlers "
+ "into schema, comments, indexes, and basic sections"
+ )
+
+ def validate_legacy_registry(
+ self,
+ registry: ToolDefinitionRegistry,
+ owner: ToolHandlerOwner,
+ ) -> None:
+ """Prove that every migrated flat capability still has a real
handler."""
+ core_definitions = tuple(
+ definition
+ for definition in registry.advertised_definitions
+ if definition.provider_name is None
+ )
+ actual_names = tuple(definition.name for definition in
core_definitions)
+ if actual_names != CURRENT_FLAT_TOOL_NAMES:
+ raise DomainCatalogError(
+ "runtime flat registry does not match the 25-tool migration
baseline"
+ )
+
+ for migration in self.legacy_migrations:
+ definition = registry.resolve(migration.legacy_tool_name)
+ if definition.handler_name != migration.legacy_handler_name:
+ raise DomainCatalogError(
+ f"legacy tool {migration.legacy_tool_name!r} changed
handler "
+ f"from {migration.legacy_handler_name!r} to "
+ f"{definition.handler_name!r}"
+ )
+ try:
+ definition.bind_handler(owner)
+ except ToolRegistryError as exc:
+ raise DomainCatalogError(
+ f"legacy tool {migration.legacy_tool_name!r} has a
dangling "
+ f"handler {migration.legacy_handler_name!r}"
+ ) from exc
+
+ def resolve_domain(self, name: str) -> DomainDefinition:
+ """Resolve one exact top-level domain name."""
+ for domain in self.domains:
+ if domain.name == name:
+ return domain
+ raise DomainCatalogError(f"unknown domain: {name}")
+
+ def resolve_child(self, domain_name: str, child_name: str) ->
ChildToolDefinition:
+ """Resolve one exact domain and child pair without fuzzy matching."""
+ domain = self.resolve_domain(domain_name)
+ for child in domain.children:
+ if child.name == child_name:
+ return child
+ raise DomainCatalogError(
+ f"unknown child {child_name!r} in domain {domain_name!r}"
+ )
+
+ def summary(self) -> DomainCatalogSummary:
+ """Return a deterministic catalog summary."""
+ domains = tuple(
+ DomainCatalogDomainSummary(
+ name=domain.name,
+ child_count=len(domain.children),
+ child_names=tuple(child.name for child in domain.children),
+ )
+ for domain in self.domains
+ )
+ return DomainCatalogSummary(
+ domain_count=len(domains),
+ child_count=sum(domain.child_count for domain in domains),
+ migrated_flat_tool_count=len(self.legacy_migrations),
+ domains=domains,
+ )
+
+ def render_markdown(self) -> str:
+ """Generate deterministic internal documentation from this catalog."""
+ lines = [
+ "# Doris MCP Hierarchical Domain Catalog",
+ "",
+ "<!-- Generated from DorisDomainCatalog; do not edit by hand. -->",
+ "",
+ "| Domain | Child count | Enablement | Discovery |",
+ "|---|---:|---|---|",
+ ]
+ for domain in self.domains:
+ lines.append(
+ f"| `{domain.name}` | {len(domain.children)} | "
+ f"`{domain.enablement_policy}` | `{domain.discovery_policy}` |"
+ )
+ lines.extend(
+ [
+ "",
+ "## Formal child tools",
+ "",
+ "| Feature ID | Handler binding | Authorization | Variants |",
+ "|---|---|---|---|",
+ ]
+ )
+ for domain in self.domains:
+ for child in domain.children:
+ variants = ", ".join(
+ f"`{variant.name}`"
+ for variant in child.support_contract.variants
+ )
+ lines.append(
+ f"| `{domain.name}.{child.name}` | `{child.handler_name}`
| "
+ f"`{child.authorization_policy}` | {variants} |"
+ )
+ lines.extend(
+ [
+ "",
+ "## Pre-1.0 migration coverage",
+ "",
+ "| Flat tool | Handler | Formal feature | Mode | Section |",
+ "|---|---|---|---|---|",
+ ]
+ )
+ for migration in self.legacy_migrations:
+ lines.append(
+ f"| `{migration.legacy_tool_name}` | "
+ f"`{migration.legacy_handler_name}` | "
+ f"`{migration.feature_id}` | `{migration.mode.value}` | "
+ f"{f'`{migration.target_section}`' if migration.target_section
else '-'} "
+ "|"
+ )
+ lines.extend(
+ [
+ "",
+ "This document describes internal 1.0 routing contracts. The "
+ "pre-1.0 flat names are migration inputs, not registered 1.0 "
+ "tool aliases.",
+ "",
+ ]
+ )
+ return "\n".join(lines)
+
+
+def _require_unique(values: tuple[str, ...], label: str) -> None:
+ if len(values) != len(set(values)):
+ raise DomainCatalogError(f"{label} must be unique")
+
+
+def _string(
+ description: str,
+ *,
+ enum: tuple[str, ...] | None = None,
+ pattern: str | None = None,
+) -> dict[str, Any]:
+ schema: dict[str, Any] = {"type": "string", "description": description}
+ if enum is not None:
+ schema["enum"] = list(enum)
+ if pattern is not None:
+ schema["pattern"] = pattern
+ return schema
+
+
+def _integer(
+ description: str,
+ *,
+ minimum: int = 0,
+ maximum: int | None = None,
+) -> dict[str, Any]:
+ schema: dict[str, Any] = {
+ "type": "integer",
+ "description": description,
+ "minimum": minimum,
+ }
+ if maximum is not None:
+ schema["maximum"] = maximum
+ return schema
+
+
+def _number(
+ description: str,
+ *,
+ minimum: float | None = None,
+ maximum: float | None = None,
+) -> dict[str, Any]:
+ schema: dict[str, Any] = {"type": "number", "description": description}
+ if minimum is not None:
+ schema["minimum"] = minimum
+ if maximum is not None:
+ schema["maximum"] = maximum
+ return schema
+
+
+def _boolean(description: str) -> dict[str, Any]:
+ return {"type": "boolean", "description": description}
+
+
+def _string_array(
+ description: str,
+ *,
+ enum: tuple[str, ...] | None = None,
+ min_items: int = 1,
+) -> dict[str, Any]:
+ item_schema: dict[str, Any] = {"type": "string"}
+ if enum is not None:
+ item_schema["enum"] = list(enum)
+ return {
+ "type": "array",
+ "description": description,
+ "items": item_schema,
+ "minItems": min_items,
+ "uniqueItems": True,
+ }
+
+
+def _object(description: str) -> dict[str, Any]:
+ return {
+ "type": "object",
+ "description": description,
+ "additionalProperties": True,
+ }
+
+
+def _array(description: str) -> dict[str, Any]:
+ return {
+ "type": "array",
+ "description": description,
+ "items": {},
+ }
+
+
+def _input_schema(
+ properties: dict[str, Any],
+ *,
+ required: tuple[str, ...] = (),
+ any_of: tuple[dict[str, Any], ...] = (),
+) -> dict[str, Any]:
+ schema: dict[str, Any] = {
+ "type": "object",
+ "properties": properties,
+ "additionalProperties": False,
+ }
+ if required:
+ schema["required"] = list(required)
+ if any_of:
+ schema["anyOf"] = list(any_of)
+ return schema
+
+
+def _result_schema(
+ data_schema: dict[str, Any],
+ *,
+ evidence: bool = False,
+) -> dict[str, Any]:
+ properties: dict[str, Any] = {
+ "status": {
+ "type": "string",
+ "enum": ["success", "partial"],
+ },
+ "data": data_schema,
+ "warnings": {
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "metadata": {
+ "type": "object",
+ "additionalProperties": True,
+ },
+ }
+ required = ["status", "data", "warnings", "metadata"]
+ if evidence:
+ properties["evidence"] = {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": True,
+ },
+ }
+ required.append("evidence")
+ return {
+ "type": "object",
+ "properties": properties,
+ "required": required,
+ "additionalProperties": False,
+ }
+
+
+_DETAIL_OUTPUT = _result_schema(
+ {"type": "object", "additionalProperties": True}
+)
+_DIAGNOSTIC_OUTPUT = _result_schema(
+ {"type": "object", "additionalProperties": True},
+ evidence=True,
+)
+_COLLECTION_OUTPUT = _result_schema(
+ {
+ "type": "object",
+ "properties": {
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": True,
+ },
+ },
+ "next_cursor": {"type": ["string", "null"]},
+ "truncated": {"type": "boolean"},
+ },
+ "required": ["items", "next_cursor", "truncated"],
+ "additionalProperties": False,
+ }
+)
+_QUERY_OUTPUT = _result_schema(
+ {
+ "type": "object",
+ "properties": {
+ "columns": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": True,
+ },
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": True,
+ },
+ },
+ "row_count": {"type": "integer", "minimum": 0},
+ "truncated": {"type": "boolean"},
+ },
+ "required": ["columns", "rows", "row_count", "truncated"],
+ "additionalProperties": False,
+ }
+)
+_TABLE_CONTEXT_OUTPUT = _result_schema(
+ {
+ "type": "object",
+ "properties": {
+ "schema": {"type": "object", "additionalProperties": True},
+ "comments": {"type": "object", "additionalProperties": True},
+ "indexes": {"type": "object", "additionalProperties": True},
+ "basic": {"type": "object", "additionalProperties": True},
+ },
+ "required": ["schema"],
+ "additionalProperties": False,
+ },
+ evidence=True,
+)
+
+
+def _child(
+ domain: str,
+ name: str,
+ title: str,
+ description: str,
+ input_schema: dict[str, Any],
+ output_schema: dict[str, Any] = _DETAIL_OUTPUT,
+ *,
+ composite_plan: CompositePlan | None = None,
+) -> ChildToolDefinition:
+ feature = DORIS_FEATURE_MATRIX.get_feature(domain, name)
+ return ChildToolDefinition(
+ name=name,
+ title=title,
+ canonical_description=description,
+ input_schema=input_schema,
+ output_schema=output_schema,
+ handler_name=f"child:{domain}:{name}",
+ support_contract=feature.support_contract,
+ authorization_policy=f"child:call:{domain}:{name}",
+ annotations=_READ_ONLY_ANNOTATIONS,
+ composite_plan=composite_plan,
+ )
+
+
+def _domain(
+ name: str,
+ title: str,
+ description: str,
+ children: tuple[ChildToolDefinition, ...],
+) -> DomainDefinition:
+ return DomainDefinition(
+ name=name,
+ title=title,
+ description=description,
+ annotations=_READ_ONLY_ANNOTATIONS,
+ children=children,
+ enablement_policy="readonly_domain_enabled",
+ discovery_policy="authorized_child_discovery",
+ )
+
+
+_TABLE_CONTEXT_PLAN = CompositePlan(
+ steps=(
+ CompositeStep(
+ name="schema",
+ handler_name="_get_table_schema_tool",
+ ),
+ CompositeStep(
+ name="table_comments",
+ handler_name="_get_table_comment_tool",
+ depends_on=("schema",),
+ ),
+ CompositeStep(
+ name="column_comments",
+ handler_name="_get_table_column_comments_tool",
+ depends_on=("schema",),
+ ),
+ CompositeStep(
+ name="indexes",
+ handler_name="_get_table_indexes_tool",
+ depends_on=("schema",),
+ ),
+ CompositeStep(
+ name="basic",
+ handler_name="_get_table_basic_info_tool",
+ depends_on=("schema",),
+ ),
+ ),
+ required_steps=("schema",),
+ optional_steps=("table_comments", "column_comments", "indexes", "basic"),
+ partial_success_policy="required_schema_optional_sections",
+ merge_handler_name="merge:doris_catalog:table_context",
+)
+
+_QUERY_DIAGNOSIS_PLAN = CompositePlan(
+ steps=(
+ CompositeStep(name="explain",
handler_name="child:doris_query:explain_query"),
+ CompositeStep(
+ name="profile",
+ handler_name="child:doris_query:get_query_profile",
+ ),
+ CompositeStep(
+ name="slow_queries",
+ handler_name="child:doris_query:list_slow_queries",
+ ),
+ CompositeStep(
+ name="cluster_context",
+ handler_name="child:doris_cluster:get_cluster_overview",
+ ),
+ ),
+ required_steps=("explain",),
+ optional_steps=("profile", "slow_queries", "cluster_context"),
+ partial_success_policy="evidence_preserving_partial",
+ merge_handler_name="merge:doris_query:performance_diagnosis",
+)
+
+_CLUSTER_OVERVIEW_PLAN = CompositePlan(
+ steps=(
+ CompositeStep(
+ name="nodes",
+ handler_name="child:doris_cluster:list_cluster_nodes",
+ ),
+ CompositeStep(
+ name="tasks",
+ handler_name="child:doris_cluster:list_active_tasks",
+ ),
+ CompositeStep(
+ name="metrics",
+ handler_name="child:doris_cluster:get_monitoring_metrics",
+ ),
+ CompositeStep(
+ name="memory",
+ handler_name="child:doris_cluster:get_memory_stats",
+ ),
+ CompositeStep(
+ name="cache",
+ handler_name="child:doris_cluster:get_cache_status",
+ ),
+ CompositeStep(
+ name="compaction",
+ handler_name="child:doris_cluster:get_compaction_status",
+ ),
+ ),
+ required_steps=("nodes",),
+ optional_steps=("tasks", "metrics", "memory", "cache", "compaction"),
+ partial_success_policy="health_summary_partial",
+ merge_handler_name="merge:doris_cluster:overview",
+)
+
+_INGESTION_DIAGNOSIS_PLAN = CompositePlan(
+ steps=(
+ CompositeStep(
+ name="ingestion",
+ handler_name="child:doris_pipeline:get_ingestion_status",
+ ),
+ CompositeStep(
+ name="freshness",
+ handler_name="child:doris_pipeline:monitor_data_freshness",
+ ),
+ CompositeStep(
+ name="dependencies",
+ handler_name="child:doris_pipeline:analyze_data_dependencies",
+ ),
+ CompositeStep(
+ name="audit",
+ handler_name="child:doris_governance:get_recent_audit_logs",
+ ),
+ ),
+ required_steps=("ingestion",),
+ optional_steps=("freshness", "dependencies", "audit"),
+ partial_success_policy="evidence_preserving_partial",
+ merge_handler_name="merge:doris_pipeline:ingestion_diagnosis",
+)
+
+_SEARCH_DIAGNOSIS_PLAN = CompositePlan(
+ steps=(
+ CompositeStep(
+ name="indexes",
+ handler_name="child:doris_search:inspect_search_indexes",
+ ),
+ CompositeStep(
+ name="explain",
+ handler_name="child:doris_query:explain_query",
+ ),
+ CompositeStep(
+ name="profile",
+ handler_name="child:doris_query:get_query_profile",
+ depends_on=("explain",),
+ ),
+ ),
+ required_steps=("indexes",),
+ optional_steps=("explain", "profile"),
+ partial_success_policy="evidence_preserving_partial",
+ merge_handler_name="merge:doris_search:query_diagnosis",
+)
+
+
+DOMAIN_DEFINITIONS = (
+ _domain(
+ "doris_catalog",
+ "Doris Catalog",
+ "Explore Doris catalogs, databases, tables, schemas, comments,
indexes, "
+ "key models, storage metadata, and object sizes.",
+ (
+ _child(
+ "doris_catalog",
+ "list_catalogs",
+ "List catalogs",
+ "List visible internal and external Doris catalogs.",
+ _input_schema(
+ {
+ "include_external": _boolean(
+ "Include external catalogs."
+ ),
+ "pattern": _string("Optional catalog-name pattern."),
+ }
+ ),
+ _COLLECTION_OUTPUT,
+ ),
+ _child(
+ "doris_catalog",
+ "list_databases",
+ "List databases",
+ "List visible databases in one exact catalog.",
+ _input_schema(
+ {
+ "catalog": _string("Catalog name."),
+ "pattern": _string("Optional database-name pattern."),
+ }
+ ),
+ _COLLECTION_OUTPUT,
+ ),
+ _child(
+ "doris_catalog",
+ "list_tables",
+ "List tables",
+ "List visible tables, views, and materialized views.",
+ _input_schema(
+ {
+ "catalog": _string("Catalog name."),
+ "database": _string("Database name."),
+ "pattern": _string("Optional object-name pattern."),
+ "types": _string_array(
+ "Object types to include.",
+ enum=("table", "view", "materialized_view"),
+ ),
+ "page": _string("Opaque pagination cursor."),
+ },
+ required=("database",),
+ ),
+ _COLLECTION_OUTPUT,
+ ),
+ _child(
+ "doris_catalog",
+ "get_table_context",
+ "Get table context",
+ "Read schema, comments, indexes, and basic table information "
+ "through a deterministic four-section composition.",
+ _input_schema(
+ {
+ "catalog": _string("Catalog name."),
+ "database": _string("Database name."),
+ "table": _string("Table name."),
+ "sections": _string_array(
+ "Sections to return.",
+ enum=("schema", "comments", "indexes", "basic"),
+ ),
+ },
+ required=("database", "table"),
+ ),
+ _TABLE_CONTEXT_OUTPUT,
+ composite_plan=_TABLE_CONTEXT_PLAN,
+ ),
+ _child(
+ "doris_catalog",
+ "get_table_size",
+ "Get table size",
+ "Read table and optional partition size and row statistics.",
+ _input_schema(
+ {
+ "catalog": _string("Catalog name."),
+ "database": _string("Database name."),
+ "table": _string("Table name."),
+ "include_partitions": _boolean(
+ "Include per-partition statistics."
+ ),
+ },
+ required=("database", "table"),
+ ),
+ ),
+ ),
+ ),
+ _domain(
+ "doris_query",
+ "Doris Query",
+ "Execute read-only Doris SQL, inspect plans and profiles, review slow "
+ "queries, and use the optional ADBC provider.",
+ (
+ _child(
+ "doris_query",
+ "execute_query",
+ "Execute query",
+ "Execute one read-only Doris SQL statement with bounded
results.",
+ _input_schema(
+ {
+ "sql": _string("Read-only SQL statement."),
+ "catalog": _string("Catalog context."),
+ "database": _string("Database context."),
+ "parameters": _object("Bound query parameters."),
+ "max_rows": _integer(
+ "Maximum returned rows.",
+ minimum=1,
+ ),
+ "timeout_ms": _integer(
+ "Execution timeout in milliseconds.",
+ minimum=1,
+ ),
+ },
+ required=("sql",),
+ ),
+ _QUERY_OUTPUT,
+ ),
+ _child(
+ "doris_query",
+ "explain_query",
+ "Explain query",
+ "Inspect a read-only query plan and evidence for pruning, "
+ "pushdown, and index selection.",
+ _input_schema(
+ {
+ "sql": _string("Read-only SQL statement."),
+ "catalog": _string("Catalog context."),
+ "database": _string("Database context."),
+ "level": _string(
+ "Explain detail level.",
+ enum=("normal", "verbose", "costs"),
+ ),
+ },
+ required=("sql",),
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ ),
+ _child(
+ "doris_query",
+ "get_query_profile",
+ "Get query profile",
+ "Read a Doris query profile by query ID or a bounded SQL run.",
+ _input_schema(
+ {
+ "query_id": _string("Existing Doris query ID."),
+ "sql": _string("Read-only SQL to profile."),
+ "recent_window_minutes": _integer(
+ "Recent lookup window in minutes.",
+ minimum=1,
+ ),
+ "include_operator_tree": _boolean(
+ "Include the operator tree."
+ ),
+ },
+ any_of=(
+ {"required": ["query_id"]},
+ {"required": ["sql"]},
+ ),
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ ),
+ _child(
+ "doris_query",
+ "diagnose_query_performance",
+ "Diagnose query performance",
+ "Combine explain, profile, slow-query, and cluster evidence "
+ "through deterministic diagnostic rules.",
+ _input_schema(
+ {
+ "query_id": _string("Existing Doris query ID."),
+ "sql": _string("Read-only SQL to diagnose."),
+ "database": _string("Database context."),
+ "include_cluster_context": _boolean(
+ "Include relevant cluster evidence."
+ ),
+ },
+ any_of=(
+ {"required": ["query_id"]},
+ {"required": ["sql"]},
+ ),
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ composite_plan=_QUERY_DIAGNOSIS_PLAN,
+ ),
+ _child(
+ "doris_query",
+ "list_slow_queries",
+ "List slow queries",
+ "List recorded slow queries with bounded filters.",
+ _input_schema(
+ {
+ "window_minutes": _integer(
+ "Lookback window in minutes.",
+ minimum=1,
+ ),
+ "limit": _integer("Maximum results.", minimum=1),
+ "min_duration_ms": _integer(
+ "Minimum duration in milliseconds."
+ ),
+ "database": _string("Database filter."),
+ "user": _string("User filter."),
+ }
+ ),
+ _COLLECTION_OUTPUT,
+ ),
+ _child(
+ "doris_query",
+ "get_adbc_connection_info",
+ "Get ADBC connection info",
+ "Report sanitized ADBC provider configuration and
reachability.",
+ _input_schema({}),
+ ),
+ _child(
+ "doris_query",
+ "execute_adbc_query",
+ "Execute ADBC query",
+ "Execute one read-only SQL statement through Arrow Flight
SQL.",
+ _input_schema(
+ {
+ "sql": _string("Read-only SQL statement."),
+ "max_rows": _integer(
+ "Maximum returned rows.",
+ minimum=1,
+ ),
+ "timeout_ms": _integer(
+ "Execution timeout in milliseconds.",
+ minimum=1,
+ ),
+ "result_format": _string(
+ "Result representation.",
+ enum=("arrow", "pandas", "dict"),
+ ),
+ },
+ required=("sql",),
+ ),
+ _QUERY_OUTPUT,
+ ),
+ ),
+ ),
+ _domain(
+ "doris_cluster",
+ "Doris Cluster",
+ "Inspect Doris health, nodes, tasks, monitoring, memory, cache, "
+ "compaction, workload, compute groups, and runtime capabilities.",
+ (
+ _child(
+ "doris_cluster",
+ "get_cluster_overview",
+ "Get cluster overview",
+ "Produce a bounded deterministic cluster health summary.",
+ _input_schema(
+ {
+ "include": _string_array(
+ "Optional overview sections.",
+ enum=(
+ "nodes",
+ "tasks",
+ "metrics",
+ "memory",
+ "cache",
+ "compaction",
+ ),
+ )
+ }
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ composite_plan=_CLUSTER_OVERVIEW_PLAN,
+ ),
+ _child(
+ "doris_cluster",
+ "list_cluster_nodes",
+ "List cluster nodes",
+ "List FE, BE, and broker nodes with visible health metadata.",
+ _input_schema(
+ {
+ "node_types": _string_array(
+ "Node types to include.",
+ enum=("fe", "be", "broker"),
+ ),
+ "include_metrics": _boolean(
+ "Include bounded node metrics."
+ ),
+ }
+ ),
+ _COLLECTION_OUTPUT,
+ ),
+ _child(
+ "doris_cluster",
+ "list_active_tasks",
+ "List active tasks",
+ "List visible query, load, schema-change, and compaction
tasks.",
+ _input_schema(
+ {
+ "task_types": _string_array(
+ "Task types to include."
+ ),
+ "states": _string_array("Task states to include."),
+ "limit": _integer("Maximum results.", minimum=1),
+ }
+ ),
+ _COLLECTION_OUTPUT,
+ ),
+ _child(
+ "doris_cluster",
+ "get_monitoring_metrics",
+ "Get monitoring metrics",
+ "Read bounded FE and BE monitoring metric snapshots.",
+ _input_schema(
+ {
+ "metric_names": _string_array("Metric names."),
+ "node_ids": _string_array("Node identifiers."),
+ "window": _string("Bounded metric window."),
+ }
+ ),
+ ),
+ _child(
+ "doris_cluster",
+ "get_memory_stats",
+ "Get memory statistics",
+ "Read process and tracker memory statistics from visible
nodes.",
+ _input_schema(
+ {
+ "node_ids": _string_array("Node identifiers."),
+ "detail": _string(
+ "Detail level.",
+ enum=("summary", "trackers", "top_consumers"),
+ ),
+ }
+ ),
+ ),
+ _child(
+ "doris_cluster",
+ "get_cache_status",
+ "Get cache status",
+ "Inspect file-cache capacity, hit state, and optional queues.",
+ _input_schema(
+ {
+ "scope": _string("Cache scope."),
+ "node_ids": _string_array("Node identifiers."),
+ "include_queues": _boolean(
+ "Include queue statistics."
+ ),
+ }
+ ),
+ ),
+ _child(
+ "doris_cluster",
+ "get_compaction_status",
+ "Get compaction status",
+ "Inspect compaction summaries or task-tracker evidence.",
+ _input_schema(
+ {
+ "database": _string("Database filter."),
+ "table": _string("Table filter."),
+ "state": _string("Compaction state filter."),
+ "limit": _integer("Maximum results.", minimum=1),
+ }
+ ),
+ ),
+ _child(
+ "doris_cluster",
+ "get_workload_group_status",
+ "Get workload group status",
+ "Read workload-group quotas, usage, queues, and limits.",
+ _input_schema(
+ {
+ "name": _string("Workload-group name."),
+ "include_usage": _boolean(
+ "Include current resource usage."
+ ),
+ }
+ ),
+ ),
+ _child(
+ "doris_cluster",
+ "get_compute_group_status",
+ "Get compute group status",
+ "Read compute-group membership and bounded usage metadata.",
+ _input_schema(
+ {
+ "name": _string("Compute-group name."),
+ "include_nodes": _boolean("Include member nodes."),
+ "include_usage": _boolean("Include resource usage."),
+ }
+ ),
+ ),
+ _child(
+ "doris_cluster",
+ "analyze_resource_growth",
+ "Analyze resource growth",
+ "Analyze recorded resource-growth evidence without inventing "
+ "missing history.",
+ _input_schema(
+ {
+ "resource": _string("Resource type."),
+ "window_days": _integer(
+ "Lookback window in days.",
+ minimum=1,
+ ),
+ "granularity": _string(
+ "Aggregation granularity.",
+ enum=("hour", "day", "week"),
+ ),
+ }
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ ),
+ _child(
+ "doris_cluster",
+ "get_runtime_capabilities",
+ "Get runtime capabilities",
+ "Return sanitized version, deployment, feature, endpoint, and "
+ "provider capability evidence.",
+ _input_schema(
+ {
+ "detail": _string(
+ "Detail level.",
+ enum=("summary", "full"),
+ )
+ }
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ ),
+ ),
+ ),
+ _domain(
+ "doris_pipeline",
+ "Doris Pipeline",
+ "Inspect ingestion, load health, materialized views, data freshness, "
+ "and upstream or downstream dependencies.",
+ (
+ _child(
+ "doris_pipeline",
+ "get_ingestion_status",
+ "Get ingestion status",
+ "Read streaming and batch load job status.",
+ _input_schema(
+ {
+ "job_types": _string_array("Load job types."),
+ "database": _string("Database filter."),
+ "table": _string("Table filter."),
+ "states": _string_array("Job states."),
+ "limit": _integer("Maximum results.", minimum=1),
+ }
+ ),
+ _COLLECTION_OUTPUT,
+ ),
+ _child(
+ "doris_pipeline",
+ "diagnose_ingestion",
+ "Diagnose ingestion",
+ "Combine load, freshness, dependency, and audit evidence using
"
+ "deterministic rules.",
+ _input_schema(
+ {
+ "job_id": _string("Load job identifier."),
+ "database": _string("Database name."),
+ "table": _string("Table name."),
+ "window_minutes": _integer(
+ "Lookback window in minutes.",
+ minimum=1,
+ ),
+ "include_dependencies": _boolean(
+ "Include dependency evidence."
+ ),
+ },
+ any_of=(
+ {"required": ["job_id"]},
+ {"required": ["database", "table"]},
+ ),
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ composite_plan=_INGESTION_DIAGNOSIS_PLAN,
+ ),
+ _child(
+ "doris_pipeline",
+ "get_materialized_view_status",
+ "Get materialized view status",
+ "Read materialized-view refresh, state, and failure metadata.",
+ _input_schema(
+ {
+ "database": _string("Database filter."),
+ "view": _string("Materialized-view filter."),
+ "states": _string_array("View states."),
+ "include_refresh_history": _boolean(
+ "Include bounded refresh history."
+ ),
+ }
+ ),
+ _COLLECTION_OUTPUT,
+ ),
+ _child(
+ "doris_pipeline",
+ "monitor_data_freshness",
+ "Monitor data freshness",
+ "Measure table freshness against a bounded threshold.",
+ _input_schema(
+ {
+ "database": _string("Database name."),
+ "table": _string("Table name."),
+ "threshold_seconds": _integer(
+ "Freshness threshold in seconds.",
+ minimum=1,
+ ),
+ "time_column": _string("Optional event-time column."),
+ },
+ required=("database", "table"),
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ ),
+ _child(
+ "doris_pipeline",
+ "analyze_data_dependencies",
+ "Analyze data dependencies",
+ "Trace bounded upstream or downstream object dependencies.",
+ _input_schema(
+ {
+ "catalog": _string("Catalog name."),
+ "database": _string("Database name."),
+ "object": _string("Object name."),
+ "direction": _string(
+ "Traversal direction.",
+ enum=("upstream", "downstream", "both"),
+ ),
+ "depth": _integer(
+ "Maximum traversal depth.",
+ minimum=1,
+ ),
+ },
+ required=("object",),
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ ),
+ ),
+ ),
+ _domain(
+ "doris_search",
+ "Doris Search",
+ "Search Doris data with text, vector, or hybrid retrieval and inspect "
+ "or diagnose search analyzers and indexes.",
+ (
+ _child(
+ "doris_search",
+ "search_data",
+ "Search data",
+ "Run bounded structured text, vector, or hybrid retrieval.",
+ _input_schema(
+ {
+ "database": _string("Database name."),
+ "table": _string("Table name."),
+ "query": _string("Text query."),
+ "mode": _string(
+ "Search mode.",
+ enum=("text", "vector", "hybrid"),
+ ),
+ "fields": _string_array("Searchable fields."),
+ "vector": _array("Query vector."),
+ "top_k": _integer(
+ "Maximum matches.",
+ minimum=1,
+ maximum=1000,
+ ),
+ "filters": _object("Structured filters."),
+ "return_fields": _string_array("Visible return
fields."),
+ },
+ required=("database", "table", "mode"),
+ ),
+ _QUERY_OUTPUT,
+ ),
+ _child(
+ "doris_search",
+ "preview_text_analysis",
+ "Preview text analysis",
+ "Preview deterministic tokenizer and analyzer output.",
+ _input_schema(
+ {
+ "text": _string("Input text."),
+ "analyzer": _string("Analyzer name."),
+ "tokenizer": _string("Tokenizer name."),
+ "token_filters": _string_array("Token filters."),
+ },
+ required=("text",),
+ ),
+ ),
+ _child(
+ "doris_search",
+ "inspect_search_indexes",
+ "Inspect search indexes",
+ "Inspect inverted and ANN index metadata and availability.",
+ _input_schema(
+ {
+ "database": _string("Database name."),
+ "table": _string("Table name."),
+ "index": _string("Optional index name."),
+ },
+ required=("database", "table"),
+ ),
+ ),
+ _child(
+ "doris_search",
+ "diagnose_search_query",
+ "Diagnose search query",
+ "Combine index, explain, and optional profile evidence.",
+ _input_schema(
+ {
+ "sql": _string("Read-only search SQL."),
+ "search_request": _object(
+ "Structured search request."
+ ),
+ "include_profile": _boolean(
+ "Include query-profile evidence."
+ ),
+ },
+ any_of=(
+ {"required": ["sql"]},
+ {"required": ["search_request"]},
+ ),
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ composite_plan=_SEARCH_DIAGNOSIS_PLAN,
+ ),
+ ),
+ ),
+ _domain(
+ "doris_governance",
+ "Doris Governance",
+ "Analyze columns and storage, inspect lineage and access evidence, "
+ "read audit metadata, and review UDF or authentication mappings.",
+ (
+ _child(
+ "doris_governance",
+ "analyze_columns",
+ "Analyze columns",
+ "Analyze bounded column completeness, cardinality, and type
risks.",
+ _input_schema(
+ {
+ "database": _string("Database name."),
+ "table": _string("Table name."),
+ "columns": _string_array("Columns to analyze."),
+ "sample_ratio": _number(
+ "Bounded sample ratio.",
+ minimum=0.0,
+ maximum=1.0,
+ ),
+ },
+ required=("database", "table"),
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ ),
+ _child(
+ "doris_governance",
+ "analyze_table_storage",
+ "Analyze table storage",
+ "Analyze table partitions, replicas, compression, indexes, and
"
+ "visible storage features.",
+ _input_schema(
+ {
+ "database": _string("Database name."),
+ "table": _string("Table name."),
+ "include_partitions": _boolean(
+ "Include partition details."
+ ),
+ "include_indexes": _boolean("Include index details."),
+ },
+ required=("database", "table"),
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ ),
+ _child(
+ "doris_governance",
+ "get_lineage_capability_status",
+ "Get lineage capability status",
+ "Report native SPI, plugin, queryable store, provider, audit "
+ "fallback, coverage, and health evidence.",
+ _input_schema({}),
+ _DIAGNOSTIC_OUTPUT,
+ ),
+ _child(
+ "doris_governance",
+ "trace_column_lineage",
+ "Trace column lineage",
+ "Trace bounded lineage while preserving each edge's evidence
type "
+ "and limitations.",
+ _input_schema(
+ {
+ "object": _string("Qualified target object."),
+ "column": _string("Optional target column."),
+ "direction": _string(
+ "Traversal direction.",
+ enum=("upstream", "downstream", "both"),
+ ),
+ "depth": _integer(
+ "Maximum traversal depth.",
+ minimum=1,
+ ),
+ "evidence_mode": _string(
+ "Evidence selection.",
+ enum=("auto", "native", "audit"),
+ ),
+ },
+ required=("object",),
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ ),
+ _child(
+ "doris_governance",
+ "analyze_data_access_patterns",
+ "Analyze data access patterns",
+ "Analyze bounded, authorized audit history for access
patterns.",
+ _input_schema(
+ {
+ "database": _string("Database filter."),
+ "table": _string("Table filter."),
+ "window_days": _integer(
+ "Lookback window in days.",
+ minimum=1,
+ ),
+ "group_by": _string(
+ "Grouping dimension.",
+ enum=("user", "object", "hour", "operation"),
+ ),
+ }
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ ),
+ _child(
+ "doris_governance",
+ "get_recent_audit_logs",
+ "Get recent audit logs",
+ "Read bounded and redacted audit events.",
+ _input_schema(
+ {
+ "window_minutes": _integer(
+ "Lookback window in minutes.",
+ minimum=1,
+ ),
+ "user": _string("User filter."),
+ "operation": _string("Operation filter."),
+ "database": _string("Database filter."),
+ "table": _string("Table filter."),
+ "limit": _integer("Maximum results.", minimum=1),
+ }
+ ),
+ _COLLECTION_OUTPUT,
+ ),
+ _child(
+ "doris_governance",
+ "list_udfs",
+ "List UDFs",
+ "List visible UDF metadata without loading or executing code.",
+ _input_schema(
+ {
+ "database": _string("Database filter."),
+ "language": _string("Language filter."),
+ "name_pattern": _string("UDF-name pattern."),
+ }
+ ),
+ _COLLECTION_OUTPUT,
+ ),
+ _child(
+ "doris_governance",
+ "get_auth_mapping_status",
+ "Get authentication mapping status",
+ "Read sanitized LDAP, OIDC, and role-mapping health metadata.",
+ _input_schema(
+ {
+ "principal": _string("Principal filter."),
+ "provider": _string(
+ "Authentication provider.",
+ enum=("ldap", "oidc"),
+ ),
+ "include_roles": _boolean(
+ "Include authorized role mappings."
+ ),
+ }
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ ),
+ ),
+ ),
+ _domain(
+ "doris_lakehouse",
+ "Doris Lakehouse",
+ "Inspect external catalogs, lakehouse tables, snapshots, partitions, "
+ "pushdown behavior, and Variant semi-structured columns.",
+ (
+ _child(
+ "doris_lakehouse",
+ "inspect_external_catalog",
+ "Inspect external catalog",
+ "Inspect sanitized external-catalog type, state, and
capabilities.",
+ _input_schema(
+ {
+ "catalog": _string("External catalog name."),
+ "include_objects": _boolean(
+ "Include a bounded object sample."
+ ),
+ "object_limit": _integer(
+ "Maximum sampled objects.",
+ minimum=1,
+ ),
+ },
+ required=("catalog",),
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ ),
+ _child(
+ "doris_lakehouse",
+ "inspect_lakehouse_table",
+ "Inspect lakehouse table",
+ "Inspect format, snapshots, partitions, statistics, and
pushdown "
+ "evidence for one external table.",
+ _input_schema(
+ {
+ "catalog": _string("External catalog name."),
+ "database": _string("Database name."),
+ "table": _string("Table name."),
+ "include_snapshots": _boolean(
+ "Include bounded snapshot metadata."
+ ),
+ "include_partitions": _boolean(
+ "Include bounded partition metadata."
+ ),
+ },
+ required=("catalog", "database", "table"),
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ ),
+ _child(
+ "doris_lakehouse",
+ "inspect_variant_column",
+ "Inspect Variant column",
+ "Inspect Variant paths, types, sparsity, and bounded sample
shape.",
+ _input_schema(
+ {
+ "catalog": _string("Catalog name."),
+ "database": _string("Database name."),
+ "table": _string("Table name."),
+ "column": _string("Variant column name."),
+ "path": _string("Optional Variant path."),
+ "sample_rows": _integer(
+ "Maximum sampled rows.",
+ minimum=1,
+ ),
+ },
+ required=("database", "table", "column"),
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ ),
+ ),
+ ),
+ _domain(
+ "doris_semantic",
+ "Doris Semantic",
+ "Discover validated Ossie models bound to Doris and read permission-"
+ "filtered semantic summaries, context, and mapping status.",
+ (
+ _child(
+ "doris_semantic",
+ "list_semantic_models",
+ "List semantic models",
+ "List authorized validated Ossie model references.",
+ _input_schema(
+ {
+ "namespace": _string("Model namespace."),
+ "tag": _string("Model tag."),
+ "pattern": _string("Model-name pattern."),
+ }
+ ),
+ _COLLECTION_OUTPUT,
+ ),
+ _child(
+ "doris_semantic",
+ "get_semantic_model_summary",
+ "Get semantic model summary",
+ "Read an exact Ossie model summary without guessing a model.",
+ _input_schema(
+ {
+ "model_ref": _string(
+ "Exact semantic model reference.",
+ pattern=r"^[A-Za-z0-9_.:/@-]+$",
+ ),
+ "include_bindings": _boolean(
+ "Include authorized Doris bindings."
+ ),
+ },
+ required=("model_ref",),
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ ),
+ _child(
+ "doris_semantic",
+ "get_semantic_context",
+ "Get semantic context",
+ "Build permission-filtered semantic context for an exact
model.",
+ _input_schema(
+ {
+ "model_ref": _string(
+ "Exact semantic model reference.",
+ pattern=r"^[A-Za-z0-9_.:/@-]+$",
+ ),
+ "request": _object(
+ "Structured measures, dimensions, filters, and
time grain."
+ ),
+ },
+ required=("model_ref", "request"),
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ ),
+ _child(
+ "doris_semantic",
+ "get_semantic_mapping_status",
+ "Get semantic mapping status",
+ "Validate Doris bindings for an exact semantic model
reference.",
+ _input_schema(
+ {
+ "model_ref": _string(
+ "Exact semantic model reference.",
+ pattern=r"^[A-Za-z0-9_.:/@-]+$",
+ ),
+ "datasource": _string("Optional datasource filter."),
+ },
+ required=("model_ref",),
+ ),
+ _DIAGNOSTIC_OUTPUT,
+ ),
+ ),
+ ),
+)
+
+
+def _migration(
+ legacy_tool_name: str,
+ legacy_handler_name: str,
+ domain: str,
+ child_name: str,
+ *,
+ mode: LegacyMigrationMode = LegacyMigrationMode.ATOMIC,
+ adapter_name: str | None = None,
+ target_section: str | None = None,
+) -> LegacyToolMigration:
+ return LegacyToolMigration(
+ legacy_tool_name=legacy_tool_name,
+ legacy_handler_name=legacy_handler_name,
+ domain=domain,
+ child_name=child_name,
+ mode=mode,
+ adapter_name=adapter_name,
+ target_section=target_section,
+ )
+
+
+LEGACY_TOOL_MIGRATIONS = (
+ _migration(
+ "exec_query",
+ "_exec_query_tool",
+ "doris_query",
+ "execute_query",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:query_arguments",
+ ),
+ _migration(
+ "get_table_schema",
+ "_get_table_schema_tool",
+ "doris_catalog",
+ "get_table_context",
+ mode=LegacyMigrationMode.COMPOSITE_SECTION,
+ target_section="schema",
+ ),
+ _migration(
+ "get_db_table_list",
+ "_get_db_table_list_tool",
+ "doris_catalog",
+ "list_tables",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:catalog_object_names",
+ ),
+ _migration(
+ "get_db_list",
+ "_get_db_list_tool",
+ "doris_catalog",
+ "list_databases",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:catalog_object_names",
+ ),
+ _migration(
+ "get_table_comment",
+ "_get_table_comment_tool",
+ "doris_catalog",
+ "get_table_context",
+ mode=LegacyMigrationMode.COMPOSITE_SECTION,
+ target_section="comments",
+ ),
+ _migration(
+ "get_table_column_comments",
+ "_get_table_column_comments_tool",
+ "doris_catalog",
+ "get_table_context",
+ mode=LegacyMigrationMode.COMPOSITE_SECTION,
+ target_section="comments",
+ ),
+ _migration(
+ "get_table_indexes",
+ "_get_table_indexes_tool",
+ "doris_catalog",
+ "get_table_context",
+ mode=LegacyMigrationMode.COMPOSITE_SECTION,
+ target_section="indexes",
+ ),
+ _migration(
+ "get_recent_audit_logs",
+ "_get_recent_audit_logs_tool",
+ "doris_governance",
+ "get_recent_audit_logs",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:audit_filters",
+ ),
+ _migration(
+ "get_catalog_list",
+ "_get_catalog_list_tool",
+ "doris_catalog",
+ "list_catalogs",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:remove_random_string",
+ ),
+ _migration(
+ "get_sql_explain",
+ "_get_sql_explain_tool",
+ "doris_query",
+ "explain_query",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:explain_arguments",
+ ),
+ _migration(
+ "get_sql_profile",
+ "_get_sql_profile_tool",
+ "doris_query",
+ "get_query_profile",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:profile_arguments",
+ ),
+ _migration(
+ "get_table_data_size",
+ "_get_table_data_size_tool",
+ "doris_catalog",
+ "get_table_size",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:table_size_arguments",
+ ),
+ _migration(
+ "get_monitoring_metrics",
+ "_get_monitoring_metrics_tool",
+ "doris_cluster",
+ "get_monitoring_metrics",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:monitoring_arguments",
+ ),
+ _migration(
+ "get_memory_stats",
+ "_get_memory_stats_tool",
+ "doris_cluster",
+ "get_memory_stats",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:memory_arguments",
+ ),
+ _migration(
+ "get_table_basic_info",
+ "_get_table_basic_info_tool",
+ "doris_catalog",
+ "get_table_context",
+ mode=LegacyMigrationMode.COMPOSITE_SECTION,
+ target_section="basic",
+ ),
+ _migration(
+ "analyze_columns",
+ "_analyze_columns_tool",
+ "doris_governance",
+ "analyze_columns",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:column_analysis_arguments",
+ ),
+ _migration(
+ "analyze_table_storage",
+ "_analyze_table_storage_tool",
+ "doris_governance",
+ "analyze_table_storage",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:storage_analysis_arguments",
+ ),
+ _migration(
+ "trace_column_lineage",
+ "_trace_column_lineage_tool",
+ "doris_governance",
+ "trace_column_lineage",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:lineage_arguments",
+ ),
+ _migration(
+ "monitor_data_freshness",
+ "_monitor_data_freshness_tool",
+ "doris_pipeline",
+ "monitor_data_freshness",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:freshness_arguments",
+ ),
+ _migration(
+ "analyze_data_access_patterns",
+ "_analyze_data_access_patterns_tool",
+ "doris_governance",
+ "analyze_data_access_patterns",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:access_analysis_arguments",
+ ),
+ _migration(
+ "analyze_data_flow_dependencies",
+ "_analyze_data_flow_dependencies_tool",
+ "doris_pipeline",
+ "analyze_data_dependencies",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:dependency_arguments",
+ ),
+ _migration(
+ "analyze_slow_queries_topn",
+ "_analyze_slow_queries_topn_tool",
+ "doris_query",
+ "list_slow_queries",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:slow_query_arguments",
+ ),
+ _migration(
+ "analyze_resource_growth_curves",
+ "_analyze_resource_growth_curves_tool",
+ "doris_cluster",
+ "analyze_resource_growth",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:resource_growth_arguments",
+ ),
+ _migration(
+ "exec_adbc_query",
+ "_exec_adbc_query_tool",
+ "doris_query",
+ "execute_adbc_query",
+ mode=LegacyMigrationMode.ADAPTED,
+ adapter_name="adapt:adbc_query_arguments",
+ ),
+ _migration(
+ "get_adbc_connection_info",
+ "_get_adbc_connection_info_tool",
+ "doris_query",
+ "get_adbc_connection_info",
+ ),
+)
+
+DORIS_DOMAIN_CATALOG = DorisDomainCatalog(
+ domains=DOMAIN_DEFINITIONS,
+ legacy_migrations=LEGACY_TOOL_MIGRATIONS,
+)
+
+
+__all__ = [
+ "CURRENT_FLAT_TOOL_NAMES",
+ "DOMAIN_DEFINITIONS",
+ "DORIS_DOMAIN_CATALOG",
+ "LEGACY_TOOL_MIGRATIONS",
+ "DomainCatalogDomainSummary",
+ "DomainCatalogError",
+ "DomainCatalogSummary",
+ "DorisDomainCatalog",
+ "LegacyMigrationMode",
+ "LegacyToolMigration",
+]
diff --git a/doris_mcp_server/tools/tool_catalog.py
b/doris_mcp_server/tools/tool_catalog.py
index 8956203..f627ab1 100644
--- a/doris_mcp_server/tools/tool_catalog.py
+++ b/doris_mcp_server/tools/tool_catalog.py
@@ -23,6 +23,7 @@ from mcp.types import Tool
from ..result_limits import configured_default_result_rows,
configured_result_limits
from ..utils.config import ADBCConfig
+from .domain_catalog import DORIS_DOMAIN_CATALOG
from .tool_provider import CustomTool
from .tool_registry import ToolDefinitionRegistry
@@ -939,8 +940,11 @@ No parameters required. Returns connection status,
configuration, and diagnostic
),
]
- return ToolDefinitionRegistry.from_tools(
+ registry = ToolDefinitionRegistry.from_tools(
tools,
handler_owner,
custom_tools=custom_tools,
)
+ DORIS_DOMAIN_CATALOG.validate_integrity()
+ DORIS_DOMAIN_CATALOG.validate_legacy_registry(registry, handler_owner)
+ return registry
diff --git a/test/tools/test_domain_catalog.py
b/test/tools/test_domain_catalog.py
new file mode 100644
index 0000000..861a1c9
--- /dev/null
+++ b/test/tools/test_domain_catalog.py
@@ -0,0 +1,621 @@
+# 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.
+
+"""Contract tests for the authoritative hierarchical domain catalog."""
+
+from __future__ import annotations
+
+from types import SimpleNamespace
+from typing import Any, cast
+from unittest.mock import Mock
+
+import pytest
+from jsonschema import Draft202012Validator
+from pydantic import ValidationError
+
+from doris_mcp_server.tools import tool_catalog
+from doris_mcp_server.tools.domain_catalog import (
+ CURRENT_FLAT_TOOL_NAMES,
+ DOMAIN_DEFINITIONS,
+ DORIS_DOMAIN_CATALOG,
+ LEGACY_TOOL_MIGRATIONS,
+ DomainCatalogError,
+ DorisDomainCatalog,
+ LegacyMigrationMode,
+ LegacyToolMigration,
+)
+from doris_mcp_server.tools.domain_models import (
+ ChildToolDefinition,
+ DomainDefinition,
+)
+from doris_mcp_server.tools.doris_feature_matrix import (
+ DORIS_FEATURE_MATRIX,
+ EXPECTED_DOMAIN_CHILDREN,
+)
+from doris_mcp_server.tools.tools_manager import DorisToolsManager
+
+
+def _config() -> SimpleNamespace:
+ return SimpleNamespace(
+ security=SimpleNamespace(max_result_rows=1000),
+ performance=SimpleNamespace(
+ max_result_bytes=4 * 1024 * 1024,
+ query_timeout=30,
+ ),
+ adbc=SimpleNamespace(
+ default_max_rows=1000,
+ default_timeout=30,
+ default_return_format="dict",
+ ),
+ )
+
+
+def _manager() -> DorisToolsManager:
+ connection_manager = Mock()
+ connection_manager.config = _config()
+ return DorisToolsManager(connection_manager)
+
+
+def _copy_catalog(
+ *,
+ domains: tuple[DomainDefinition, ...] | None = None,
+ migrations: tuple[LegacyToolMigration, ...] | None = None,
+) -> DorisDomainCatalog:
+ return DORIS_DOMAIN_CATALOG.model_copy(
+ update={
+ "domains": domains or DORIS_DOMAIN_CATALOG.domains,
+ "legacy_migrations": migrations
+ or DORIS_DOMAIN_CATALOG.legacy_migrations,
+ }
+ )
+
+
+def _wire_input(child: ChildToolDefinition) -> dict[str, Any]:
+ return cast(dict[str, Any], child.to_wire()["input_schema"])
+
+
+def _wire_output(child: ChildToolDefinition) -> dict[str, Any]:
+ return cast(dict[str, Any], child.to_wire()["output_schema"])
+
+
+def test_catalog_has_exact_ordered_eight_domain_forty_seven_child_shape() ->
None:
+ summary = DORIS_DOMAIN_CATALOG.summary()
+
+ assert summary.domain_count == 8
+ assert summary.child_count == 47
+ assert summary.migrated_flat_tool_count == 25
+ assert tuple(domain.name for domain in summary.domains) == tuple(
+ EXPECTED_DOMAIN_CHILDREN
+ )
+ assert {
+ domain.name: domain.child_names for domain in summary.domains
+ } == dict(EXPECTED_DOMAIN_CHILDREN)
+ assert tuple(
+ len(domain.children) for domain in DORIS_DOMAIN_CATALOG.domains
+ ) == (5, 7, 11, 5, 4, 8, 3, 4)
+
+
+def test_adbc_is_inside_query_and_cluster_has_exactly_eleven_children() ->
None:
+ query = DORIS_DOMAIN_CATALOG.resolve_domain("doris_query")
+ cluster = DORIS_DOMAIN_CATALOG.resolve_domain("doris_cluster")
+
+ assert "get_adbc_connection_info" in {
+ child.name for child in query.children
+ }
+ assert "execute_adbc_query" in {child.name for child in query.children}
+ assert len(cluster.children) == 11
+
+
+def test_every_child_uses_the_exact_feature_matrix_contract() -> None:
+ feature_contracts = {
+ feature.feature_id: feature.support_contract
+ for feature in DORIS_FEATURE_MATRIX.features
+ }
+
+ for domain in DORIS_DOMAIN_CATALOG.domains:
+ for child in domain.children:
+ feature_id = f"{domain.name}.{child.name}"
+ assert child.support_contract is feature_contracts[feature_id]
+ assert child.handler_name == f"child:{feature_id.replace('.',
':')}"
+ assert child.authorization_policy == (
+ f"child:call:{feature_id.replace('.', ':')}"
+ )
+
+
+def test_every_domain_and_child_is_strictly_read_only() -> None:
+ for domain in DORIS_DOMAIN_CATALOG.domains:
+ assert domain.annotations.read_only is True
+ assert domain.annotations.idempotent is True
+ assert domain.annotations.destructive is False
+ assert domain.annotations.requires_confirmation is False
+ assert len(domain.children) <= 12
+ for child in domain.children:
+ assert child.annotations == domain.annotations
+
+
+def test_every_child_schema_is_valid_closed_and_non_placeholder() -> None:
+ for domain in DORIS_DOMAIN_CATALOG.domains:
+ for child in domain.children:
+ input_schema = _wire_input(child)
+ output_schema = _wire_output(child)
+ Draft202012Validator.check_schema(input_schema)
+ Draft202012Validator.check_schema(output_schema)
+ assert input_schema["type"] == "object"
+ assert input_schema["additionalProperties"] is False
+ assert output_schema["type"] == "object"
+ assert output_schema["additionalProperties"] is False
+ assert len(child.title) > 3
+ assert len(child.canonical_description) > 20
+
+
[email protected](
+ ("domain_name", "child_name", "required"),
+ [
+ ("doris_catalog", "list_tables", {"database"}),
+ ("doris_catalog", "get_table_context", {"database", "table"}),
+ ("doris_query", "execute_query", {"sql"}),
+ ("doris_query", "execute_adbc_query", {"sql"}),
+ ("doris_pipeline", "monitor_data_freshness", {"database", "table"}),
+ ("doris_lakehouse", "inspect_lakehouse_table", {"catalog", "database",
"table"}),
+ (
+ "doris_semantic",
+ "get_semantic_model_summary",
+ {"model_ref"},
+ ),
+ ("doris_semantic", "get_semantic_context", {"model_ref", "request"}),
+ (
+ "doris_semantic",
+ "get_semantic_mapping_status",
+ {"model_ref"},
+ ),
+ ],
+)
+def test_important_child_required_parameters_are_explicit(
+ domain_name: str,
+ child_name: str,
+ required: set[str],
+) -> None:
+ child = DORIS_DOMAIN_CATALOG.resolve_child(domain_name, child_name)
+
+ assert set(_wire_input(child)["required"]) == required
+
+
+def test_semantic_content_children_require_explicit_model_ref() -> None:
+ semantic = DORIS_DOMAIN_CATALOG.resolve_domain("doris_semantic")
+ schemas = {
+ child.name: _wire_input(child) for child in semantic.children
+ }
+
+ assert "required" not in schemas["list_semantic_models"]
+ for name in (
+ "get_semantic_model_summary",
+ "get_semantic_context",
+ "get_semantic_mapping_status",
+ ):
+ assert "model_ref" in schemas[name]["required"]
+ assert "model_ref" in schemas[name]["properties"]
+
+
+def test_table_context_has_four_sections_and_five_deterministic_steps() ->
None:
+ child = DORIS_DOMAIN_CATALOG.resolve_child(
+ "doris_catalog",
+ "get_table_context",
+ )
+ plan = child.composite_plan
+
+ assert plan is not None
+ section_enum =
_wire_input(child)["properties"]["sections"]["items"]["enum"]
+ assert tuple(section_enum) == ("schema", "comments", "indexes", "basic")
+ assert tuple(step.name for step in plan.steps) == (
+ "schema",
+ "table_comments",
+ "column_comments",
+ "indexes",
+ "basic",
+ )
+ assert plan.required_steps == ("schema",)
+ assert set(plan.optional_steps) == {
+ "table_comments",
+ "column_comments",
+ "indexes",
+ "basic",
+ }
+
+
[email protected](
+ ("domain_name", "child_name", "required_steps"),
+ [
+ (
+ "doris_query",
+ "diagnose_query_performance",
+ ("explain",),
+ ),
+ (
+ "doris_cluster",
+ "get_cluster_overview",
+ ("nodes",),
+ ),
+ (
+ "doris_pipeline",
+ "diagnose_ingestion",
+ ("ingestion",),
+ ),
+ (
+ "doris_search",
+ "diagnose_search_query",
+ ("indexes",),
+ ),
+ ],
+)
+def test_composite_diagnostics_have_deterministic_required_steps(
+ domain_name: str,
+ child_name: str,
+ required_steps: tuple[str, ...],
+) -> None:
+ child = DORIS_DOMAIN_CATALOG.resolve_child(domain_name, child_name)
+
+ assert child.composite_plan is not None
+ assert child.composite_plan.required_steps == required_steps
+ assert child.composite_plan.partial_success_policy
+
+
+def test_legacy_migrations_cover_exact_flat_baseline_and_real_handlers() ->
None:
+ manager = _manager()
+
+ assert tuple(
+ migration.legacy_tool_name
+ for migration in DORIS_DOMAIN_CATALOG.legacy_migrations
+ ) == CURRENT_FLAT_TOOL_NAMES
+ assert tuple(
+ definition.name
+ for definition in manager.tool_registry.advertised_definitions
+ if definition.provider_name is None
+ ) == CURRENT_FLAT_TOOL_NAMES
+ DORIS_DOMAIN_CATALOG.validate_legacy_registry(
+ manager.tool_registry,
+ manager,
+ )
+
+
+def test_table_context_migrations_cover_exact_sections() -> None:
+ migrations = tuple(
+ migration
+ for migration in LEGACY_TOOL_MIGRATIONS
+ if migration.feature_id == "doris_catalog.get_table_context"
+ )
+
+ assert tuple(migration.legacy_tool_name for migration in migrations) == (
+ "get_table_schema",
+ "get_table_comment",
+ "get_table_column_comments",
+ "get_table_indexes",
+ "get_table_basic_info",
+ )
+ assert tuple(migration.target_section for migration in migrations) == (
+ "schema",
+ "comments",
+ "comments",
+ "indexes",
+ "basic",
+ )
+ assert all(
+ migration.mode is LegacyMigrationMode.COMPOSITE_SECTION
+ for migration in migrations
+ )
+
+
+def test_renamed_or_reshaped_migrations_declare_adapters() -> None:
+ adapted = tuple(
+ migration
+ for migration in LEGACY_TOOL_MIGRATIONS
+ if migration.mode is LegacyMigrationMode.ADAPTED
+ )
+
+ assert adapted
+ assert all(migration.adapter_name for migration in adapted)
+ assert all(
+ migration.target_section is None for migration in adapted
+ )
+
+
+def test_catalog_markdown_is_deterministic_and_complete() -> None:
+ first = DORIS_DOMAIN_CATALOG.render_markdown()
+ second = DORIS_DOMAIN_CATALOG.render_markdown()
+
+ assert first == second
+ assert first.startswith("# Doris MCP Hierarchical Domain Catalog")
+ assert first.count("| `doris_") >= 47 + 8
+ assert "`doris_query.execute_adbc_query`" in first
+ assert "`get_table_schema`" in first
+ assert "`doris_catalog.get_table_context`" in first
+ assert "pre-1.0 flat names are migration inputs" in first
+
+
+def test_summary_wire_schema_and_serialization_are_stable() -> None:
+ summary = DORIS_DOMAIN_CATALOG.summary()
+
+ Draft202012Validator.check_schema(summary.wire_schema())
+ assert summary.to_wire() == DORIS_DOMAIN_CATALOG.summary().to_wire()
+ assert summary.to_canonical_json() == (
+ DORIS_DOMAIN_CATALOG.summary().to_canonical_json()
+ )
+
+
+def test_resolvers_reject_unknown_names_without_fuzzy_matching() -> None:
+ with pytest.raises(DomainCatalogError, match="unknown domain"):
+ DORIS_DOMAIN_CATALOG.resolve_domain("catalog")
+ with pytest.raises(DomainCatalogError, match="unknown child"):
+ DORIS_DOMAIN_CATALOG.resolve_child("doris_catalog", "list_table")
+
+
+def test_duplicate_domain_is_rejected_even_when_validation_was_bypassed() ->
None:
+ invalid = _copy_catalog(
+ domains=(
+ *DORIS_DOMAIN_CATALOG.domains,
+ DORIS_DOMAIN_CATALOG.domains[0],
+ )
+ )
+
+ with pytest.raises(DomainCatalogError, match="domain names must be
unique"):
+ invalid.validate_integrity()
+
+
+def test_duplicate_child_is_rejected_even_when_validation_was_bypassed() ->
None:
+ domain = DORIS_DOMAIN_CATALOG.domains[0]
+ invalid_domain = domain.model_copy(
+ update={"children": (*domain.children, domain.children[0])}
+ )
+ invalid = _copy_catalog(
+ domains=(invalid_domain, *DORIS_DOMAIN_CATALOG.domains[1:])
+ )
+
+ with pytest.raises(DomainCatalogError, match="exact ordered"):
+ invalid.validate_integrity()
+
+
+def test_domain_above_twelve_child_budget_is_rejected() -> None:
+ cluster_index = 2
+ cluster = DORIS_DOMAIN_CATALOG.domains[cluster_index]
+ invalid_cluster = cluster.model_copy(
+ update={
+ "children": (
+ *cluster.children,
+ cluster.children[0],
+ cluster.children[1],
+ )
+ }
+ )
+ domains = list(DORIS_DOMAIN_CATALOG.domains)
+ domains[cluster_index] = invalid_cluster
+ invalid = _copy_catalog(domains=tuple(domains))
+
+ with pytest.raises(DomainCatalogError, match="12-child limit"):
+ invalid.validate_integrity()
+
+
+def test_noncanonical_handler_binding_is_rejected() -> None:
+ domain = DORIS_DOMAIN_CATALOG.domains[0]
+ child = domain.children[0].model_copy(
+ update={"handler_name": "child:wrong:binding"}
+ )
+ invalid_domain = domain.model_copy(
+ update={"children": (child, *domain.children[1:])}
+ )
+ invalid = _copy_catalog(
+ domains=(invalid_domain, *DORIS_DOMAIN_CATALOG.domains[1:])
+ )
+
+ with pytest.raises(DomainCatalogError, match="non-canonical handler"):
+ invalid.validate_integrity()
+
+
+def test_noncanonical_authorization_policy_is_rejected() -> None:
+ domain = DORIS_DOMAIN_CATALOG.domains[0]
+ child = domain.children[0].model_copy(
+ update={"authorization_policy": "child:call:wrong"}
+ )
+ invalid_domain = domain.model_copy(
+ update={"children": (child, *domain.children[1:])}
+ )
+ invalid = _copy_catalog(
+ domains=(invalid_domain, *DORIS_DOMAIN_CATALOG.domains[1:])
+ )
+
+ with pytest.raises(DomainCatalogError, match="authorization policy"):
+ invalid.validate_integrity()
+
+
+def test_wrong_feature_matrix_contract_is_rejected() -> None:
+ domain = DORIS_DOMAIN_CATALOG.domains[0]
+ child = domain.children[0].model_copy(
+ update={
+ "support_contract": DORIS_DOMAIN_CATALOG.domains[0]
+ .children[1]
+ .support_contract
+ }
+ )
+ invalid_domain = domain.model_copy(
+ update={"children": (child, *domain.children[1:])}
+ )
+ invalid = _copy_catalog(
+ domains=(invalid_domain, *DORIS_DOMAIN_CATALOG.domains[1:])
+ )
+
+ with pytest.raises(DomainCatalogError, match="feature-matrix contract"):
+ invalid.validate_integrity()
+
+
+def test_non_read_only_domain_and_child_are_rejected() -> None:
+ domain = DORIS_DOMAIN_CATALOG.domains[0]
+ writable_annotations = domain.annotations.model_copy(
+ update={"read_only": False}
+ )
+ invalid_domain = domain.model_copy(
+ update={"annotations": writable_annotations}
+ )
+ invalid = _copy_catalog(
+ domains=(invalid_domain, *DORIS_DOMAIN_CATALOG.domains[1:])
+ )
+
+ with pytest.raises(DomainCatalogError, match="not marked read-only"):
+ invalid.validate_integrity()
+
+ child = domain.children[0].model_copy(
+ update={"annotations": writable_annotations}
+ )
+ invalid_domain = domain.model_copy(
+ update={"children": (child, *domain.children[1:])}
+ )
+ invalid = _copy_catalog(
+ domains=(invalid_domain, *DORIS_DOMAIN_CATALOG.domains[1:])
+ )
+ with pytest.raises(DomainCatalogError, match="risk contract"):
+ invalid.validate_integrity()
+
+
+def test_migration_baseline_drift_is_rejected() -> None:
+ invalid = _copy_catalog(
+ migrations=DORIS_DOMAIN_CATALOG.legacy_migrations[:-1]
+ )
+
+ with pytest.raises(DomainCatalogError, match="exact ordered 25-tool"):
+ invalid.validate_integrity()
+
+
+def test_unknown_migration_target_is_rejected() -> None:
+ first = LEGACY_TOOL_MIGRATIONS[0].model_copy(
+ update={"child_name": "missing_child"}
+ )
+ invalid = _copy_catalog(
+ migrations=(first, *LEGACY_TOOL_MIGRATIONS[1:])
+ )
+
+ with pytest.raises(DomainCatalogError, match="targets unknown child"):
+ invalid.validate_integrity()
+
+
+def test_composite_migration_requires_composite_target() -> None:
+ first = LEGACY_TOOL_MIGRATIONS[0].model_copy(
+ update={
+ "mode": LegacyMigrationMode.COMPOSITE_SECTION,
+ "target_section": "schema",
+ }
+ )
+ invalid = _copy_catalog(
+ migrations=(first, *LEGACY_TOOL_MIGRATIONS[1:])
+ )
+
+ with pytest.raises(DomainCatalogError, match="non-composite child"):
+ invalid.validate_integrity()
+
+
+def test_table_context_section_mapping_drift_is_rejected() -> None:
+ migrations = list(LEGACY_TOOL_MIGRATIONS)
+ migrations[1] = migrations[1].model_copy(
+ update={"target_section": "basic"}
+ )
+ invalid = _copy_catalog(migrations=tuple(migrations))
+
+ with pytest.raises(DomainCatalogError, match="exact five legacy handlers"):
+ invalid.validate_integrity()
+
+
+def test_migration_model_rejects_invalid_mode_fields() -> None:
+ common = {
+ "legacy_tool_name": "old_tool",
+ "legacy_handler_name": "_old_tool",
+ "domain": "doris_catalog",
+ "child_name": "list_tables",
+ }
+ with pytest.raises(ValidationError, match="target_section"):
+ LegacyToolMigration(
+ **common,
+ mode=LegacyMigrationMode.COMPOSITE_SECTION,
+ )
+ with pytest.raises(ValidationError, match="only valid"):
+ LegacyToolMigration(
+ **common,
+ mode=LegacyMigrationMode.ATOMIC,
+ target_section="schema",
+ )
+ with pytest.raises(ValidationError, match="adapter_name"):
+ LegacyToolMigration(
+ **common,
+ mode=LegacyMigrationMode.ADAPTED,
+ )
+ with pytest.raises(ValidationError, match="adapter_name"):
+ LegacyToolMigration(
+ **common,
+ mode=LegacyMigrationMode.ATOMIC,
+ adapter_name="unexpected_adapter",
+ )
+
+
+def test_runtime_registry_handler_name_drift_is_rejected() -> None:
+ manager = _manager()
+ first = LEGACY_TOOL_MIGRATIONS[0].model_copy(
+ update={"legacy_handler_name": "_changed_handler"}
+ )
+ invalid = _copy_catalog(
+ migrations=(first, *LEGACY_TOOL_MIGRATIONS[1:])
+ )
+
+ with pytest.raises(DomainCatalogError, match="changed handler"):
+ invalid.validate_legacy_registry(manager.tool_registry, manager)
+
+
+def test_runtime_dangling_handler_is_rejected() -> None:
+ manager = _manager()
+ manager.__dict__["_exec_query_tool"] = None
+
+ with pytest.raises(DomainCatalogError, match="dangling handler"):
+ DORIS_DOMAIN_CATALOG.validate_legacy_registry(
+ manager.tool_registry,
+ manager,
+ )
+
+
+def test_runtime_flat_registry_drift_is_rejected() -> None:
+ manager = _manager()
+ registry = manager.tool_registry
+ registry._ordered_names = registry._ordered_names[1:]
+
+ with pytest.raises(DomainCatalogError, match="25-tool migration baseline"):
+ DORIS_DOMAIN_CATALOG.validate_legacy_registry(registry, manager)
+
+
+def test_server_startup_rejects_injected_duplicate_catalog(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ domain = DORIS_DOMAIN_CATALOG.domains[0]
+ invalid_domain = domain.model_copy(
+ update={"children": (*domain.children, domain.children[0])}
+ )
+ invalid = _copy_catalog(
+ domains=(invalid_domain, *DORIS_DOMAIN_CATALOG.domains[1:])
+ )
+ monkeypatch.setattr(tool_catalog, "DORIS_DOMAIN_CATALOG", invalid)
+
+ with pytest.raises(DomainCatalogError, match="exact ordered"):
+ _manager()
+
+
+def test_catalog_model_constructor_rejects_invalid_shape() -> None:
+ with pytest.raises(ValidationError, match="exact ordered"):
+ DorisDomainCatalog(
+ domains=DOMAIN_DEFINITIONS[:-1],
+ legacy_migrations=LEGACY_TOOL_MIGRATIONS,
+ )
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]