""" Pydantic models for TopSQL query tool input validation. Validates cluster context, time range, and limit for TopSQL CPU-time analysis. """ from datetime import datetime from typing import Literal, Optional from pydantic import BaseModel, Field, field_validator, model_validator InstanceType = Literal["tidb", "tikv"] TOPSQL_CPU_TIME_METRIC = "topsql_cpu_time_ms" TOPSQL_SQL_META_METRIC = "topsql_sql_meta" class TopSQLRequest(BaseModel): """ Input model for query_topsql requests. Validates cluster context, ISO8601 time range, instance type, and result limit. """ cluster_id: str = Field( ..., description="Cluster identifier", ) env: str = Field( ..., description="Environment name used in biz_type resolution (e.g. prod, staging, dev)", ) deploy_type: str = Field( ..., description=( "Cluster deploy type used in biz_type resolution " "(dedicated, premium, resource_pool, starter, essential)" ), ) start: str = Field( ..., description="Start timestamp in RFC3339 format (e.g., '2024-01-01T00:00:00Z')", examples=["2024-01-01T00:00:00Z", "2024-01-01T12:30:45+00:00"], ) end: str = Field( ..., description="End timestamp in RFC3339 format (e.g., '2024-01-01T23:59:59Z')", examples=["2024-01-01T23:59:59Z", "2024-01-02T00:00:00+00:00"], ) instance_type: Optional[InstanceType] = Field( default=None, description=( "Filter by TiDB component type: 'tidb' or 'tikv'. " "When set, only TopSQL data from matching instances is included. " "When omitted, data from all instance types is aggregated." ), ) limit: int = Field( default=20, ge=1, le=100, description="Number of top SQL statements to return (1–100, default 20)", ) @field_validator("start", "end") @classmethod def validate_timestamp(cls, v: str) -> str: """Validate RFC3339 timestamp format.""" try: datetime.fromisoformat(v.replace("Z", "+00:00")) return v except ValueError as e: raise ValueError(f"Invalid timestamp format: {v}. Must be RFC3339.") from e @field_validator("env", "deploy_type", mode="before") @classmethod def normalize_scope_text(cls, v: str) -> str: return v.strip().lower() @field_validator("instance_type", mode="before") @classmethod def normalize_instance_type(cls, v: Optional[str]) -> Optional[str]: if v is None or not str(v).strip(): return None normalized = str(v).strip().lower() if normalized not in ("tidb", "tikv"): raise ValueError(f"instance_type must be 'tidb' or 'tikv', got '{normalized}'") return normalized @field_validator("cluster_id", mode="before") @classmethod def normalize_cluster_id(cls, v: object) -> str: if not isinstance(v, str): raise ValueError("cluster_id must be a string. Quote numeric IDs explicitly.") return str(v).strip() @model_validator(mode="after") def validate_time_range(self) -> "TopSQLRequest": """Validate that start time is before or equal to end time.""" try: start_dt = datetime.fromisoformat(self.start.replace("Z", "+00:00")) end_dt = datetime.fromisoformat(self.end.replace("Z", "+00:00")) if start_dt > end_dt: raise ValueError("Start timestamp must be before or equal to end timestamp") return self except ValueError as e: if "Start timestamp" in str(e): raise return self def compute_range_duration(self) -> str: """ Compute a PromQL-compatible range duration string from start/end. Returns a duration like '1h', '30m', '2h' rounded up to the nearest minute. """ start_dt = datetime.fromisoformat(self.start.replace("Z", "+00:00")) end_dt = datetime.fromisoformat(self.end.replace("Z", "+00:00")) delta = end_dt - start_dt total_seconds = int(delta.total_seconds()) if total_seconds <= 0: return "5m" if total_seconds >= 3600: hours = (total_seconds + 3599) // 3600 return f"{hours}h" minutes = max(1, (total_seconds + 59) // 60) return f"{minutes}m" class TopSQLEntry(BaseModel): """A single entry in the TopSQL result table.""" rank: int = Field(..., description="Rank position (1-based)") sql_digest: str = Field(..., description="SQL digest hash") sql_text: Optional[str] = Field( default=None, description="Normalized SQL text (from topsql_sql_meta labels). None if unavailable.", ) cpu_time_ms: float = Field(..., description="CPU time in milliseconds")