| # 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. |
| |
| """Strict read-only execution and evidence collection for the Query domain.""" |
| |
| from __future__ import annotations |
| |
| import asyncio |
| import hashlib |
| import json |
| import math |
| import re |
| import uuid |
| from collections.abc import Mapping |
| from dataclasses import dataclass |
| from datetime import datetime, timedelta |
| from decimal import Decimal |
| from typing import Any |
| from urllib.parse import quote |
| |
| import sqlparse |
| from sqlparse import tokens as sql_tokens |
| |
| from ..result_limits import ( |
| ResultLimitError, |
| ResultLimits, |
| configured_default_result_rows, |
| configured_result_limits, |
| resolve_result_limits, |
| ) |
| from .adbc_query_tools import DorisADBCQueryTools |
| from .db import DorisConnectionManager, QueryResult |
| from .doris_http_client import ( |
| DorisHTTPClient, |
| DorisHTTPResponse, |
| configured_fe_http_hosts, |
| database_config_for_request, |
| ) |
| from .security import get_current_auth_context |
| from .sql_security_utils import ( |
| SQLSecurityError, |
| quote_identifier, |
| validate_identifier, |
| ) |
| |
| _MAX_SQL_LENGTH = 1024 * 1024 |
| _MAX_QUERY_ID_LENGTH = 128 |
| _MAX_SLOW_QUERY_LIMIT = 1000 |
| _MAX_SLOW_QUERY_WINDOW_MINUTES = 90 * 24 * 60 |
| _MAX_SLOW_QUERY_TEXT = 8192 |
| _QUERY_ID_RE = re.compile(r"^[A-Za-z0-9_-]{1,128}$") |
| _NAMED_PARAMETER_RE = re.compile(r"(?<!%)%\(([A-Za-z_][A-Za-z0-9_]*)\)s") |
| _POSITIONAL_PARAMETER_RE = re.compile(r"(?<!%)%s") |
| _SIDE_EFFECT_FUNCTIONS = ( |
| "BENCHMARK", |
| "GET_LOCK", |
| "LOAD_FILE", |
| "MASTER_POS_WAIT", |
| "RELEASE_LOCK", |
| "SLEEP", |
| ) |
| |
| |
| class QueryRuntimeFailure(RuntimeError): |
| """Sanitized Query-domain failure with a stable reason code.""" |
| |
| def __init__( |
| self, |
| message: str, |
| *, |
| reason_code: str, |
| status_code: int, |
| retryable: bool = False, |
| ) -> None: |
| super().__init__(message) |
| self.reason_code = reason_code |
| self.status_code = status_code |
| self.retryable = retryable |
| |
| |
| @dataclass(frozen=True, slots=True) |
| class ReadOnlyStatement: |
| """One canonical, single-statement read-only SQL request.""" |
| |
| sql: str |
| operation: str |
| |
| |
| class ReadOnlySQLGuard: |
| """Fail-closed SQL parser used by every formal Query execution path.""" |
| |
| @classmethod |
| def validate( |
| cls, |
| sql: str, |
| *, |
| query_target: bool = False, |
| ) -> ReadOnlyStatement: |
| if not isinstance(sql, str) or not sql.strip(): |
| raise _argument_failure("SQL must be a non-empty string.") |
| if len(sql.encode("utf-8")) > _MAX_SQL_LENGTH: |
| raise _argument_failure("SQL exceeds the maximum accepted size.") |
| if "/*!" in sql: |
| raise _read_only_failure("Executable version comments are not allowed.") |
| |
| statements = [ |
| statement |
| for statement in sqlparse.parse(sql) |
| if _has_executable_sql(statement) |
| ] |
| if len(statements) != 1: |
| raise _read_only_failure("Exactly one read-only SQL statement is required.") |
| |
| statement = statements[0] |
| canonical = str(statement).strip() |
| if canonical.endswith(";"): |
| canonical = canonical[:-1].rstrip() |
| code_tokens = [ |
| token |
| for token in statement.flatten() |
| if not token.is_whitespace |
| and token.ttype not in sql_tokens.Comment |
| and token.ttype not in sql_tokens.Literal.String |
| ] |
| if not code_tokens: |
| raise _argument_failure("SQL contains no executable statement.") |
| |
| parsed_type = statement.get_type().upper() |
| first = str(code_tokens[0].value).strip().upper() |
| operation = parsed_type if parsed_type != "UNKNOWN" else first |
| if first == "WITH" and parsed_type == "SELECT": |
| operation = "SELECT" |
| if operation not in {"SELECT", "SHOW", "DESC", "DESCRIBE", "EXPLAIN"}: |
| raise _read_only_failure( |
| f"SQL operation {operation or 'UNKNOWN'} is not read-only." |
| ) |
| if query_target and operation not in {"SELECT", "SHOW", "DESC", "DESCRIBE"}: |
| raise _read_only_failure( |
| "This operation requires a query target, not an EXPLAIN statement." |
| ) |
| |
| if operation == "EXPLAIN": |
| executable_sql = "".join( |
| str(token.value) |
| for token in statement.flatten() |
| if token.ttype not in sql_tokens.Comment |
| ) |
| target = re.fullmatch( |
| r"\s*EXPLAIN(?:\s+VERBOSE)?\s+(.+?)\s*", |
| executable_sql, |
| flags=re.IGNORECASE | re.DOTALL, |
| ) |
| if target is None: |
| raise _read_only_failure( |
| "EXPLAIN must contain one supported read-only query target." |
| ) |
| cls.validate(target.group(1), query_target=True) |
| |
| code = " ".join(str(token.value) for token in code_tokens) |
| upper_code = code.upper() |
| if re.search(r"\bINTO\s+(OUTFILE|DUMPFILE)\b", upper_code): |
| raise _read_only_failure("Result export clauses are not allowed.") |
| if ":=" in code: |
| raise _read_only_failure("Session-variable assignment is not allowed.") |
| if re.search(r"\bFOR\s+UPDATE\b", upper_code): |
| raise _read_only_failure("Locking reads are not allowed.") |
| if re.search(r"\bLOCK\s+IN\s+SHARE\s+MODE\b", upper_code): |
| raise _read_only_failure("Locking reads are not allowed.") |
| |
| if operation == "SELECT": |
| contains_nested_mutation = any( |
| ( |
| token.ttype in sql_tokens.Keyword.DML |
| and str(token.value).strip().upper() != "SELECT" |
| ) |
| or token.ttype in sql_tokens.Keyword.DDL |
| for token in code_tokens |
| ) |
| if contains_nested_mutation: |
| raise _read_only_failure("SQL contains a disallowed operation.") |
| for function_name in _SIDE_EFFECT_FUNCTIONS: |
| if re.search( |
| rf"\b{re.escape(function_name)}\s*\(", |
| upper_code, |
| ): |
| raise _read_only_failure( |
| "SQL contains a disallowed side-effecting function." |
| ) |
| |
| return ReadOnlyStatement(sql=canonical, operation=operation) |
| |
| @staticmethod |
| def validate_parameters( |
| sql: str, |
| parameters: Mapping[str, Any] | None, |
| ) -> dict[str, Any] | None: |
| parameter_code = _parameter_code(sql) |
| placeholders = set(_NAMED_PARAMETER_RE.findall(parameter_code)) |
| if _POSITIONAL_PARAMETER_RE.search(parameter_code): |
| raise _argument_failure( |
| "Positional SQL parameters are not supported; use named " |
| "%(name)s placeholders." |
| ) |
| if parameters is None: |
| if placeholders: |
| raise _argument_failure("SQL parameters are missing.") |
| return None |
| if not isinstance(parameters, Mapping): |
| raise _argument_failure("Query parameters must be an object.") |
| |
| normalized: dict[str, Any] = {} |
| for raw_name, value in parameters.items(): |
| if not isinstance(raw_name, str) or not re.fullmatch( |
| r"[A-Za-z_][A-Za-z0-9_]{0,63}", |
| raw_name, |
| ): |
| raise _argument_failure("Query parameter names are invalid.") |
| if value is not None and not isinstance( |
| value, |
| str | int | float | bool, |
| ): |
| raise _argument_failure("Query parameter values must be JSON scalars.") |
| if isinstance(value, float) and not math.isfinite(value): |
| raise _argument_failure("Query parameter numbers must be finite.") |
| normalized[raw_name] = value |
| |
| supplied = set(normalized) |
| if supplied != placeholders: |
| raise _argument_failure( |
| "SQL placeholders and supplied parameters must match exactly." |
| ) |
| return normalized |
| |
| @staticmethod |
| def prepare_driver_sql( |
| sql: str, |
| parameters: Mapping[str, Any] | None, |
| ) -> str: |
| """Escape non-placeholder percent signs for the PyMySQL formatter.""" |
| if parameters is None: |
| return sql |
| parameter_code = _parameter_code(sql) |
| placeholder_ends = { |
| match.start(): match.end() |
| for match in _NAMED_PARAMETER_RE.finditer(parameter_code) |
| } |
| output: list[str] = [] |
| offset = 0 |
| while offset < len(sql): |
| placeholder_end = placeholder_ends.get(offset) |
| if placeholder_end is not None: |
| output.append(sql[offset:placeholder_end]) |
| offset = placeholder_end |
| continue |
| character = sql[offset] |
| output.append("%%" if character == "%" else character) |
| offset += 1 |
| return "".join(output) |
| |
| |
| class DorisQueryRuntime: |
| """Production Query-domain runtime shared by hierarchical and flat tools.""" |
| |
| def __init__( |
| self, |
| connection_manager: DorisConnectionManager, |
| adbc_query_tools: DorisADBCQueryTools, |
| ) -> None: |
| self._connection_manager = connection_manager |
| self._adbc_query_tools = adbc_query_tools |
| self._session_prefix = f"formal_query_{uuid.uuid4().hex[:8]}" |
| |
| async def execute_query( |
| self, |
| *, |
| sql: str, |
| catalog: str | None = None, |
| database: str | None = None, |
| parameters: Mapping[str, Any] | None = None, |
| max_rows: int | None = None, |
| timeout_ms: int | None = None, |
| ) -> dict[str, Any]: |
| """Execute one bounded read-only statement over the MySQL protocol.""" |
| statement = ReadOnlySQLGuard.validate(sql) |
| bound_parameters = ReadOnlySQLGuard.validate_parameters( |
| statement.sql, |
| parameters, |
| ) |
| driver_sql = ReadOnlySQLGuard.prepare_driver_sql( |
| statement.sql, |
| bound_parameters, |
| ) |
| limits = self._resolve_limits(max_rows=max_rows, timeout_ms=timeout_ms) |
| result = await self._execute_statement( |
| driver_sql, |
| parameters=bound_parameters, |
| catalog=catalog, |
| database=database, |
| limits=limits, |
| purpose="execute", |
| ) |
| return _query_result( |
| result, |
| limits=limits, |
| protocol="mysql", |
| operation=statement.operation, |
| ) |
| |
| async def explain_query( |
| self, |
| *, |
| sql: str, |
| catalog: str | None = None, |
| database: str | None = None, |
| level: str | None = None, |
| ) -> dict[str, Any]: |
| """Return a bounded plan plus deterministic, evidence-only facets.""" |
| statement = ReadOnlySQLGuard.validate(sql, query_target=True) |
| ReadOnlySQLGuard.validate_parameters(statement.sql, None) |
| requested_level = "normal" if level is None else level |
| if requested_level not in {"normal", "verbose", "costs"}: |
| raise _argument_failure("Explain level is invalid.") |
| |
| warnings: list[str] = [] |
| effective_level = requested_level |
| if requested_level == "normal": |
| explain_prefix = "EXPLAIN" |
| else: |
| explain_prefix = "EXPLAIN VERBOSE" |
| if requested_level == "costs": |
| effective_level = "verbose" |
| warnings.append( |
| "Doris has no EXPLAIN COSTS keyword; the verbose plan and " |
| "its reported cardinality/cost fields were returned." |
| ) |
| |
| explain_row_budget = min( |
| 10_000, |
| configured_result_limits(self._connection_manager.config).max_rows, |
| ) |
| limits = self._resolve_limits( |
| max_rows=explain_row_budget, |
| timeout_ms=None, |
| ) |
| result = await self._execute_statement( |
| f"{explain_prefix} {statement.sql}", |
| parameters=None, |
| catalog=catalog, |
| database=database, |
| limits=limits, |
| purpose="explain", |
| mask_result=False, |
| ) |
| plan_rows = [_json_mapping(row) for row in (result.data or [])] |
| plan_truncated = bool(result.metadata.get("truncated")) |
| truncation_reason = result.metadata.get("truncation_reason") |
| if plan_truncated: |
| warnings.append( |
| "The EXPLAIN output was truncated at the configured result " |
| f"boundary ({truncation_reason or 'unknown limit'})." |
| ) |
| plan_text = "\n".join( |
| " | ".join(str(value) for value in row.values()) for row in plan_rows |
| ) |
| data = { |
| "requested_level": requested_level, |
| "effective_level": effective_level, |
| "plan_rows": plan_rows, |
| "plan_text": plan_text, |
| "facets": _explain_facets(plan_text), |
| } |
| metadata = { |
| "source": "doris_explain", |
| "plan_row_count": len(plan_rows), |
| "plan_truncated": plan_truncated, |
| "truncation_reason": truncation_reason, |
| "execution_time_seconds": getattr(result, "execution_time", 0.0), |
| } |
| return _diagnostic_result( |
| data, |
| status="partial" if warnings else "success", |
| warnings=warnings, |
| metadata=metadata, |
| evidence=( |
| { |
| "source": "doris_sql", |
| "kind": "explain", |
| "operation": statement.operation, |
| }, |
| ), |
| ) |
| |
| async def get_query_profile( |
| self, |
| *, |
| query_id: str | None = None, |
| sql: str | None = None, |
| recent_window_minutes: int | None = None, |
| include_operator_tree: bool = False, |
| database: str | None = None, |
| ) -> dict[str, Any]: |
| """Read one profile by ID or execute one bounded query with profiling.""" |
| if bool(query_id) == bool(sql): |
| raise _argument_failure("Provide exactly one of query_id or sql.") |
| window = _bounded_integer( |
| (60 if recent_window_minutes is None else recent_window_minutes), |
| "recent_window_minutes", |
| minimum=1, |
| maximum=_MAX_SLOW_QUERY_WINDOW_MINUTES, |
| ) |
| warnings: list[str] = [] |
| query_summary: dict[str, Any] | None = None |
| resolved_query_id = query_id |
| |
| if query_id is not None: |
| resolved_query_id = _validate_query_id(query_id) |
| else: |
| statement = ReadOnlySQLGuard.validate(str(sql), query_target=True) |
| ReadOnlySQLGuard.validate_parameters(statement.sql, None) |
| limits = self._resolve_limits(max_rows=1, timeout_ms=None) |
| trace_id = str(uuid.uuid4()) |
| query_result = await self._execute_profiled_statement( |
| statement.sql, |
| trace_id=trace_id, |
| database=database, |
| limits=limits, |
| ) |
| query_summary = { |
| "row_count": len(query_result.data or []), |
| "execution_time_seconds": query_result.execution_time, |
| "truncated": bool(query_result.metadata.get("truncated")), |
| } |
| resolved_query_id = await self._wait_for_query_id(trace_id) |
| if resolved_query_id is None: |
| return _diagnostic_result( |
| { |
| "query_id": None, |
| "profile": None, |
| "operator_tree": [], |
| "query_summary": query_summary, |
| }, |
| status="partial", |
| warnings=( |
| "The query completed, but Doris did not publish a " |
| "query ID before the bounded lookup expired.", |
| ), |
| metadata={ |
| "source": "doris_profile_api", |
| "recent_window_minutes": window, |
| }, |
| evidence=( |
| { |
| "source": "doris_sql", |
| "kind": "profiled_query", |
| }, |
| ), |
| ) |
| |
| profile = await self._fetch_profile(_validate_query_id(resolved_query_id)) |
| profile_text = profile["profile_text"] |
| maximum = _profile_text_limit(self._connection_manager.config) |
| truncated = len(profile_text) > maximum |
| visible_profile = profile_text[:maximum] |
| if truncated: |
| warnings.append( |
| "Profile text was truncated at the configured response limit." |
| ) |
| operator_tree = ( |
| _profile_operator_tree(visible_profile) if include_operator_tree else [] |
| ) |
| return _diagnostic_result( |
| { |
| "query_id": resolved_query_id, |
| "profile": visible_profile, |
| "profile_facets": _profile_facets(visible_profile), |
| "operator_tree": operator_tree, |
| "query_summary": query_summary, |
| }, |
| status="partial" if warnings else "success", |
| warnings=warnings, |
| metadata={ |
| "source": "doris_profile_api", |
| "profile_size": len(profile_text), |
| "profile_truncated": truncated, |
| "recent_window_minutes": window, |
| }, |
| evidence=( |
| { |
| "source": "doris_fe_http", |
| "kind": "query_profile", |
| "query_id": resolved_query_id, |
| }, |
| ), |
| ) |
| |
| async def list_slow_queries( |
| self, |
| *, |
| window_minutes: int | None = None, |
| limit: int | None = None, |
| min_duration_ms: int | None = None, |
| database: str | None = None, |
| user: str | None = None, |
| ) -> dict[str, Any]: |
| """List bounded audit-log records without swallowing provider failures.""" |
| window = _bounded_integer( |
| 60 if window_minutes is None else window_minutes, |
| "window_minutes", |
| minimum=1, |
| maximum=_MAX_SLOW_QUERY_WINDOW_MINUTES, |
| ) |
| row_limit = _bounded_integer( |
| 20 if limit is None else limit, |
| "limit", |
| minimum=1, |
| maximum=_MAX_SLOW_QUERY_LIMIT, |
| ) |
| duration = _bounded_integer( |
| 1000 if min_duration_ms is None else min_duration_ms, |
| "min_duration_ms", |
| minimum=0, |
| maximum=86_400_000, |
| ) |
| if database is not None: |
| try: |
| validate_identifier(database, "database name") |
| except SQLSecurityError as exc: |
| raise _argument_failure( |
| "Slow-query database filter is invalid." |
| ) from exc |
| if user is not None and (not user.strip() or len(user) > 128): |
| raise _argument_failure("User filter is invalid.") |
| |
| start_time = datetime.now() - timedelta(minutes=window) |
| normalized_user = user.strip() if user is not None else None |
| sql = ( |
| "SELECT `query_id`, `time`, `user`, `catalog`, `db`, `state`, " |
| "`query_time`, `cpu_time_ms`, `scan_bytes`, `scan_rows`, " |
| "`return_rows`, `peak_memory_bytes`, `sql_hash`, `sql_digest`, " |
| "`stmt` FROM internal.__internal_schema.audit_log " |
| "WHERE `time` >= %s AND `query_time` >= %s AND `is_query` = 1 " |
| "AND (%s IS NULL OR `db` = %s) " |
| "AND (%s IS NULL OR `user` = %s) " |
| "ORDER BY `query_time` DESC, `time` DESC, `query_id` " |
| "LIMIT %s" |
| ) |
| params = ( |
| start_time, |
| duration, |
| database, |
| database, |
| normalized_user, |
| normalized_user, |
| row_limit + 1, |
| ) |
| limits = ResultLimits( |
| max_rows=row_limit + 1, |
| max_bytes=configured_result_limits( |
| self._connection_manager.config |
| ).max_bytes, |
| timeout_seconds=min( |
| 30, |
| configured_result_limits( |
| self._connection_manager.config |
| ).timeout_seconds, |
| ), |
| ) |
| result = await self._execute_statement( |
| sql, |
| parameters=tuple(params), |
| catalog=None, |
| database=None, |
| limits=limits, |
| purpose="slow_queries", |
| mask_result=False, |
| trusted_internal=True, |
| ) |
| raw_rows = list(result.data or []) |
| truncated = len(raw_rows) > row_limit |
| raw_rows = await self._mask_audit_rows(raw_rows) |
| items = [_slow_query_item(row) for row in raw_rows[:row_limit]] |
| return _collection_result( |
| items, |
| truncated=truncated, |
| metadata={ |
| "source": "internal.__internal_schema.audit_log", |
| "window_minutes": window, |
| "min_duration_ms": duration, |
| "returned_items": len(items), |
| }, |
| ) |
| |
| async def get_adbc_connection_info( |
| self, |
| *, |
| explicit_adbc: bool, |
| ) -> dict[str, Any]: |
| """Return a secret-free summary of the optional ADBC provider.""" |
| _require_explicit_adbc_intent(explicit_adbc) |
| raw = await self._adbc_query_tools.get_adbc_connection_info() |
| if not isinstance(raw, Mapping): |
| raise QueryRuntimeFailure( |
| "ADBC provider returned an invalid status response.", |
| reason_code="ADBC_PROVIDER_INVALID_RESPONSE", |
| status_code=502, |
| ) |
| port_status = raw.get("port_status") |
| module_status = raw.get("module_status") |
| port_data = port_status if isinstance(port_status, Mapping) else {} |
| module_data = module_status if isinstance(module_status, Mapping) else {} |
| configuration = raw.get("configuration") |
| configuration_data = configuration if isinstance(configuration, Mapping) else {} |
| status = str(raw.get("status", "not_ready")) |
| data = { |
| "enabled": bool( |
| getattr( |
| getattr(self._connection_manager.config, "adbc", None), |
| "enabled", |
| False, |
| ) |
| ), |
| "status": status, |
| "driver_ready": bool(module_data.get("success")), |
| "flight_sql_reachable": bool(port_data.get("success")), |
| "fe_port_configured": bool(configuration_data.get("fe_arrow_flight_port")), |
| "be_port_configured": bool(configuration_data.get("be_arrow_flight_port")), |
| "reachable_be_count": int(port_data.get("be_available_count", 0) or 0), |
| "driver_versions": { |
| "manager": str(module_data.get("adbc_manager_version", "unknown")), |
| "flight_sql": str(module_data.get("flight_sql_version", "unknown")), |
| }, |
| } |
| warnings = ( |
| () |
| if status == "ready" |
| else ("ADBC is not ready on the active Doris route.",) |
| ) |
| return _detail_result( |
| data, |
| status="partial" if warnings else "success", |
| warnings=warnings, |
| metadata={"source": "adbc_runtime_probe"}, |
| ) |
| |
| async def execute_adbc_query( |
| self, |
| *, |
| explicit_adbc: bool, |
| sql: str, |
| max_rows: int | None = None, |
| timeout_ms: int | None = None, |
| result_format: str | None = None, |
| ) -> dict[str, Any]: |
| """Execute one strict read-only query through Arrow Flight SQL.""" |
| _require_explicit_adbc_intent(explicit_adbc) |
| statement = ReadOnlySQLGuard.validate(sql) |
| ReadOnlySQLGuard.validate_parameters(statement.sql, None) |
| if result_format not in {None, "arrow", "pandas", "dict"}: |
| raise _argument_failure("ADBC result format is invalid.") |
| timeout = ( |
| math.ceil( |
| _bounded_integer( |
| timeout_ms, |
| "timeout_ms", |
| minimum=1, |
| maximum=300_000, |
| ) |
| / 1000 |
| ) |
| if timeout_ms is not None |
| else None |
| ) |
| raw = await self._adbc_query_tools.exec_adbc_query( |
| statement.sql, |
| max_rows=max_rows, |
| timeout=timeout, |
| return_format=result_format, |
| ) |
| if not isinstance(raw, Mapping) or raw.get("success") is not True: |
| raise _classify_adbc_failure(raw) |
| |
| result = raw.get("result") |
| result_data = result if isinstance(result, Mapping) else {} |
| column_names = list(result_data.get("column_names", []) or []) |
| column_types = list(result_data.get("column_types", []) or []) |
| rows_value = result_data.get("data") |
| if not isinstance(rows_value, list): |
| rows_value = result_data.get("data_preview") |
| rows = [ |
| _json_mapping(row) |
| for row in (rows_value if isinstance(rows_value, list) else []) |
| if isinstance(row, Mapping) |
| ] |
| source_row_count = int(result_data.get("num_rows", len(rows)) or 0) |
| representation_truncated = source_row_count > len(rows) |
| truncated = bool(raw.get("truncated")) or representation_truncated |
| warnings: list[str] = [] |
| if representation_truncated: |
| warnings.append( |
| "The Arrow representation exposes a bounded row preview in " |
| "the MCP JSON response." |
| ) |
| return _query_output( |
| columns=[ |
| { |
| "name": str(name), |
| "type": ( |
| str(column_types[index]) |
| if index < len(column_types) |
| else "unknown" |
| ), |
| } |
| for index, name in enumerate(column_names) |
| ], |
| rows=rows, |
| truncated=truncated, |
| warnings=warnings, |
| metadata={ |
| "source": "adbc_arrow_flight_sql", |
| "protocol": "adbc", |
| "result_format": str(result_data.get("format", result_format or "")), |
| "source_row_count": source_row_count, |
| "truncation_reason": raw.get("truncation_reason"), |
| "execution_time_seconds": raw.get("execution_time"), |
| }, |
| ) |
| |
| async def diagnose_query_performance( |
| self, |
| *, |
| query_id: str | None = None, |
| sql: str | None = None, |
| database: str | None = None, |
| include_cluster_context: bool = False, |
| ) -> dict[str, Any]: |
| """Compose deterministic evidence without model-generated guesses.""" |
| if bool(query_id) == bool(sql): |
| raise _argument_failure("Provide exactly one of query_id or sql.") |
| |
| audit_row: dict[str, Any] | None = None |
| audit_record: dict[str, Any] | None = None |
| resolved_query_id = query_id |
| target_sql = sql |
| resolved_catalog: str | None = None |
| resolved_database = database |
| warnings: list[str] = [] |
| steps: dict[str, Any] = {} |
| |
| if query_id is not None: |
| resolved_query_id = _validate_query_id(query_id) |
| audit_row = await self._find_audit_record(resolved_query_id) |
| if audit_row is not None: |
| audit_record = _slow_query_item(audit_row) |
| candidate = audit_record.get("sql") |
| target_sql = str(candidate) if candidate else None |
| resolved_catalog = _optional_identifier(audit_record.get("catalog")) |
| if resolved_database is None: |
| resolved_database = _optional_identifier( |
| audit_record.get("database") |
| ) |
| if not target_sql or bool( |
| audit_record and audit_record.get("sql_truncated") |
| ): |
| target_sql = await self._fetch_query_sql(resolved_query_id) |
| if not target_sql: |
| raise QueryRuntimeFailure( |
| "Doris did not expose SQL evidence for the requested query ID.", |
| reason_code="QUERY_SQL_EVIDENCE_NOT_FOUND", |
| status_code=404, |
| ) |
| |
| statement = ReadOnlySQLGuard.validate(str(target_sql), query_target=True) |
| explain = await self.explain_query( |
| sql=statement.sql, |
| catalog=resolved_catalog, |
| database=resolved_database, |
| level="verbose", |
| ) |
| warnings.extend(_result_warnings(explain)) |
| steps["explain"] = { |
| "status": explain["status"], |
| "source": explain["metadata"].get("source"), |
| } |
| |
| profile: dict[str, Any] | None = None |
| try: |
| profile = await self.get_query_profile( |
| query_id=resolved_query_id, |
| sql=None if resolved_query_id else statement.sql, |
| include_operator_tree=True, |
| database=resolved_database, |
| ) |
| except QueryRuntimeFailure as exc: |
| warnings.append( |
| f"Query profile evidence is unavailable ({exc.reason_code})." |
| ) |
| steps["profile"] = { |
| "status": "unavailable", |
| "reason_code": exc.reason_code, |
| } |
| else: |
| warnings.extend(_result_warnings(profile)) |
| steps["profile"] = { |
| "status": profile["status"], |
| "source": profile["metadata"].get("source"), |
| } |
| if resolved_query_id is None: |
| candidate_id = profile["data"].get("query_id") |
| if isinstance(candidate_id, str): |
| resolved_query_id = candidate_id |
| |
| if audit_row is None and resolved_query_id is not None: |
| audit_row = await self._find_audit_record(resolved_query_id) |
| if audit_row is not None: |
| audit_record = _slow_query_item(audit_row) |
| steps["slow_query"] = { |
| "status": "success" if audit_record is not None else "unavailable", |
| "source": ( |
| "internal.__internal_schema.audit_log" |
| if audit_record is not None |
| else None |
| ), |
| } |
| if audit_record is None: |
| warnings.append("No matching audit-log evidence was found.") |
| |
| cluster_context: dict[str, Any] | None = None |
| if include_cluster_context: |
| cluster_context = { |
| "status": "unavailable", |
| "reason_code": "CLUSTER_DOMAIN_NOT_YET_IMPLEMENTED", |
| } |
| warnings.append( |
| "Cluster context is deferred to the dedicated Cluster-domain " |
| "implementation." |
| ) |
| steps["cluster_context"] = ( |
| dict(cluster_context) |
| if cluster_context is not None |
| else {"status": "not_requested"} |
| ) |
| |
| explain_data = explain["data"] |
| profile_data = profile["data"] if profile is not None else {} |
| findings = _diagnostic_findings( |
| explain_text=str(explain_data.get("plan_text", "")), |
| profile_text=str(profile_data.get("profile") or ""), |
| audit_record=audit_record, |
| ) |
| recommendations = _diagnostic_recommendations(findings) |
| evidence = list(explain.get("evidence", [])) |
| if profile is not None: |
| evidence.extend(profile.get("evidence", [])) |
| if audit_record is not None: |
| evidence.append( |
| { |
| "source": "doris_audit_log", |
| "kind": "query_record", |
| "query_id": resolved_query_id, |
| } |
| ) |
| visible_audit_record = audit_record |
| if audit_row is not None: |
| visible_rows = await self._mask_audit_rows([audit_row]) |
| visible_audit_record = ( |
| _slow_query_item(visible_rows[0]) if visible_rows else None |
| ) |
| warnings = list(dict.fromkeys(warnings)) |
| evidence_is_partial = any( |
| step.get("status") == "partial" |
| for step in steps.values() |
| if isinstance(step, Mapping) |
| ) |
| status = "partial" if warnings or evidence_is_partial else "success" |
| return _diagnostic_result( |
| { |
| "query_id": resolved_query_id, |
| "steps": steps, |
| "findings": findings, |
| "recommendations": recommendations, |
| "audit_record": visible_audit_record, |
| "cluster_context": cluster_context, |
| }, |
| status=status, |
| warnings=warnings, |
| metadata={ |
| "source": "deterministic_query_diagnosis", |
| "rule_version": "query-diagnosis-v1", |
| }, |
| evidence=tuple(evidence), |
| ) |
| |
| async def _mask_audit_rows( |
| self, |
| rows: list[dict[str, Any]], |
| ) -> list[dict[str, Any]]: |
| """Apply request-scoped masking before audit evidence becomes visible.""" |
| if not rows: |
| return [] |
| auth_context = get_current_auth_context() |
| security_manager = getattr( |
| self._connection_manager, |
| "security_manager", |
| None, |
| ) |
| if auth_context is None or security_manager is None: |
| return [dict(row) for row in rows] |
| masked = await security_manager.apply_data_masking( |
| [dict(row) for row in rows], |
| auth_context, |
| ) |
| return [dict(row) for row in masked] |
| |
| def _resolve_limits( |
| self, |
| *, |
| max_rows: int | None, |
| timeout_ms: int | None, |
| ) -> ResultLimits: |
| default_rows = configured_default_result_rows(self._connection_manager.config) |
| ceilings = configured_result_limits(self._connection_manager.config) |
| if timeout_ms is None: |
| timeout = min(30, ceilings.timeout_seconds) |
| else: |
| bounded_timeout_ms = _bounded_integer( |
| timeout_ms, |
| "timeout_ms", |
| minimum=1, |
| maximum=300_000, |
| ) |
| timeout = math.ceil(bounded_timeout_ms / 1000) |
| try: |
| return resolve_result_limits( |
| self._connection_manager.config, |
| max_rows=max_rows if max_rows is not None else default_rows, |
| max_bytes=None, |
| timeout_seconds=timeout, |
| ) |
| except ResultLimitError as exc: |
| raise _argument_failure(str(exc)) from exc |
| |
| async def _execute_statement( |
| self, |
| sql: str, |
| *, |
| parameters: Mapping[str, Any] | tuple[Any, ...] | None, |
| catalog: str | None, |
| database: str | None, |
| limits: ResultLimits, |
| purpose: str, |
| mask_result: bool = True, |
| trusted_internal: bool = False, |
| ) -> QueryResult: |
| context_statements = _context_statements(catalog, database) |
| auth_context = get_current_auth_context() |
| session_id = f"{self._session_prefix}_{purpose}" |
| try: |
| async with asyncio.timeout(limits.timeout_seconds): |
| async with ( |
| self._connection_manager.get_connection_context_for_auth_context( |
| session_id, |
| auth_context, |
| ) as connection |
| ): |
| try: |
| for context_sql in context_statements: |
| await connection.execute( |
| context_sql, |
| auth_context=None, |
| mask_result=False, |
| internal_session_control=True, |
| ) |
| return await connection.execute( |
| sql, |
| params=parameters, |
| auth_context=(None if trusted_internal else auth_context), |
| mask_result=mask_result, |
| max_rows=limits.max_rows, |
| max_bytes=limits.max_bytes, |
| ) |
| finally: |
| if context_statements: |
| connection.is_healthy = False |
| except TimeoutError as exc: |
| raise QueryRuntimeFailure( |
| "Doris query execution timed out.", |
| reason_code="QUERY_TIMEOUT", |
| status_code=504, |
| retryable=True, |
| ) from exc |
| except QueryRuntimeFailure: |
| raise |
| except Exception as exc: |
| raise _classify_query_failure(exc) from exc |
| |
| async def _execute_profiled_statement( |
| self, |
| sql: str, |
| *, |
| trace_id: str, |
| database: str | None, |
| limits: ResultLimits, |
| ) -> QueryResult: |
| context_statements = _context_statements(None, database) |
| auth_context = get_current_auth_context() |
| session_id = f"{self._session_prefix}_profile" |
| try: |
| async with asyncio.timeout(limits.timeout_seconds): |
| async with ( |
| self._connection_manager.get_connection_context_for_auth_context( |
| session_id, |
| auth_context, |
| ) as connection |
| ): |
| try: |
| for context_sql in context_statements: |
| await connection.execute( |
| context_sql, |
| auth_context=None, |
| mask_result=False, |
| ) |
| await connection.execute( |
| f'SET session_context="trace_id:{trace_id}"', |
| auth_context=None, |
| mask_result=False, |
| internal_session_control=True, |
| ) |
| await connection.execute( |
| "SET enable_profile=true", |
| auth_context=None, |
| mask_result=False, |
| internal_session_control=True, |
| ) |
| return await connection.execute( |
| sql, |
| auth_context=auth_context, |
| max_rows=limits.max_rows, |
| max_bytes=limits.max_bytes, |
| ) |
| finally: |
| connection.is_healthy = False |
| except TimeoutError as exc: |
| raise QueryRuntimeFailure( |
| "Profiled Doris query timed out.", |
| reason_code="QUERY_TIMEOUT", |
| status_code=504, |
| retryable=True, |
| ) from exc |
| except Exception as exc: |
| raise _classify_query_failure(exc) from exc |
| |
| async def _wait_for_query_id(self, trace_id: str) -> str | None: |
| for delay in (0.0, 0.2, 0.5): |
| if delay: |
| await asyncio.sleep(delay) |
| query_id = await self._fetch_query_id(trace_id) |
| if query_id is not None: |
| return query_id |
| return None |
| |
| async def _fetch_query_id(self, trace_id: str) -> str | None: |
| response = await self._profile_http_get( |
| f"/rest/v2/manager/query/trace_id/{quote(trace_id, safe='')}" |
| ) |
| payload = _http_json_if_present(response) |
| api_code = _api_code(payload) if payload is not None else None |
| if api_code in {401, 403}: |
| return None |
| if api_code == 404 or response.status == 404: |
| return None |
| if response.status != 200: |
| raise _profile_http_failure(response.status) |
| if payload is None: |
| raise QueryRuntimeFailure( |
| "Doris query profile service returned an invalid response.", |
| reason_code="QUERY_PROFILE_INVALID_RESPONSE", |
| status_code=502, |
| ) |
| if api_code not in {None, 0}: |
| raise QueryRuntimeFailure( |
| "Doris query profile service returned an invalid response.", |
| reason_code="QUERY_PROFILE_INVALID_RESPONSE", |
| status_code=502, |
| ) |
| data = payload.get("data") |
| if isinstance(data, str) and data: |
| return _validate_query_id(data) |
| if isinstance(data, Mapping): |
| query_ids = data.get("query_ids") |
| if isinstance(query_ids, list) and query_ids: |
| return _validate_query_id(str(query_ids[0])) |
| return None |
| |
| async def _fetch_profile(self, query_id: str) -> dict[str, str]: |
| response = await self._profile_http_get( |
| f"/rest/v2/manager/query/profile/text/{quote(query_id, safe='')}" |
| ) |
| payload = _http_json_if_present(response) |
| api_code = _api_code(payload) if payload is not None else None |
| if api_code in {401, 403}: |
| raise QueryRuntimeFailure( |
| "Doris denied access to query profile evidence.", |
| reason_code="QUERY_PROFILE_PERMISSION_DENIED", |
| status_code=403, |
| ) |
| if api_code == 404 or response.status == 404: |
| raise QueryRuntimeFailure( |
| "The requested Doris query profile was not found.", |
| reason_code="QUERY_PROFILE_NOT_FOUND", |
| status_code=404, |
| ) |
| if response.status != 200: |
| raise _profile_http_failure(response.status) |
| text = response.text() |
| if payload is not None: |
| if api_code not in {None, 0}: |
| raise QueryRuntimeFailure( |
| "Doris query profile service returned an invalid response.", |
| reason_code="QUERY_PROFILE_INVALID_RESPONSE", |
| status_code=502, |
| ) |
| data = payload.get("data") |
| if isinstance(data, str): |
| text = data |
| elif isinstance(data, Mapping): |
| text = str(data.get("profile", "")) |
| if not text.strip(): |
| raise QueryRuntimeFailure( |
| "The requested Doris query profile was not found.", |
| reason_code="QUERY_PROFILE_NOT_FOUND", |
| status_code=404, |
| ) |
| return {"query_id": query_id, "profile_text": text} |
| |
| async def _fetch_query_sql(self, query_id: str) -> str: |
| response = await self._profile_http_get( |
| f"/rest/v2/manager/query/sql/{quote(query_id, safe='')}" |
| ) |
| payload = _http_json_if_present(response) |
| api_code = _api_code(payload) if payload is not None else None |
| if api_code in {401, 403}: |
| raise QueryRuntimeFailure( |
| "Doris denied access to query SQL evidence.", |
| reason_code="QUERY_PROFILE_PERMISSION_DENIED", |
| status_code=403, |
| ) |
| if api_code == 404 or response.status == 404: |
| raise QueryRuntimeFailure( |
| "SQL evidence for the requested Doris query was not found.", |
| reason_code="QUERY_SQL_EVIDENCE_NOT_FOUND", |
| status_code=404, |
| ) |
| if response.status != 200: |
| raise _profile_http_failure(response.status) |
| if payload is None: |
| raise QueryRuntimeFailure( |
| "Doris query profile service returned an invalid response.", |
| reason_code="QUERY_PROFILE_INVALID_RESPONSE", |
| status_code=502, |
| ) |
| if api_code not in {None, 0}: |
| raise QueryRuntimeFailure( |
| "Doris query profile service returned an invalid response.", |
| reason_code="QUERY_PROFILE_INVALID_RESPONSE", |
| status_code=502, |
| ) |
| data = payload.get("data") |
| sql = str(data.get("sql", "")) if isinstance(data, Mapping) else str(data or "") |
| if not sql.strip(): |
| raise QueryRuntimeFailure( |
| "SQL evidence for the requested Doris query was not found.", |
| reason_code="QUERY_SQL_EVIDENCE_NOT_FOUND", |
| status_code=404, |
| ) |
| return sql |
| |
| async def _profile_http_get(self, path: str) -> DorisHTTPResponse: |
| try: |
| auth_context = get_current_auth_context() |
| if auth_context is not None and auth_context.auth_method == "doris_oauth": |
| raise QueryRuntimeFailure( |
| "Query profile HTTP access is unavailable for this " |
| "credential route.", |
| reason_code="QUERY_PROFILE_CREDENTIAL_ROUTE_UNAVAILABLE", |
| status_code=503, |
| ) |
| db_config = database_config_for_request(self._connection_manager) |
| hosts = configured_fe_http_hosts(db_config) |
| client = DorisHTTPClient.from_database_config(db_config) |
| return await client.get_first_available( |
| role="fe", |
| hosts=hosts, |
| port=db_config.fe_http_port, |
| path=path, |
| headers={"Accept": "application/json, text/plain"}, |
| ) |
| except QueryRuntimeFailure: |
| raise |
| except Exception as exc: |
| raise QueryRuntimeFailure( |
| "Doris query profile service is temporarily unavailable.", |
| reason_code="QUERY_PROFILE_BACKEND_UNAVAILABLE", |
| status_code=503, |
| retryable=True, |
| ) from exc |
| |
| async def _find_audit_record( |
| self, |
| query_id: str, |
| ) -> dict[str, Any] | None: |
| limits = ResultLimits( |
| max_rows=1, |
| max_bytes=configured_result_limits( |
| self._connection_manager.config |
| ).max_bytes, |
| timeout_seconds=min( |
| 30, |
| configured_result_limits( |
| self._connection_manager.config |
| ).timeout_seconds, |
| ), |
| ) |
| sql = ( |
| "SELECT `query_id`, `time`, `user`, `catalog`, `db`, `state`, " |
| "`query_time`, `cpu_time_ms`, `scan_bytes`, `scan_rows`, " |
| "`return_rows`, `peak_memory_bytes`, `sql_hash`, `sql_digest`, " |
| "`stmt` FROM internal.__internal_schema.audit_log " |
| "WHERE `query_id` = %s ORDER BY `time` DESC LIMIT 1" |
| ) |
| try: |
| result = await self._execute_statement( |
| sql, |
| parameters=(query_id,), |
| catalog=None, |
| database=None, |
| limits=limits, |
| purpose="query_audit", |
| mask_result=False, |
| trusted_internal=True, |
| ) |
| except QueryRuntimeFailure: |
| return None |
| if not result.data: |
| return None |
| return dict(result.data[0]) |
| |
| |
| def _argument_failure(message: str) -> QueryRuntimeFailure: |
| return QueryRuntimeFailure( |
| message, |
| reason_code="QUERY_ARGUMENT_INVALID", |
| status_code=400, |
| ) |
| |
| |
| def _parameter_code(sql: str) -> str: |
| """Return executable SQL text while blanking comments and string literals.""" |
| statements = sqlparse.parse(sql) |
| if len(statements) != 1: |
| return sql |
| return "".join( |
| ( |
| " " * len(str(token.value)) |
| if token.ttype in sql_tokens.Comment |
| or token.ttype in sql_tokens.Literal.String |
| else str(token.value) |
| ) |
| for token in statements[0].flatten() |
| ) |
| |
| |
| def _has_executable_sql(statement: Any) -> bool: |
| """Return whether a parsed statement contains more than comments or delimiters.""" |
| return any( |
| not token.is_whitespace |
| and token.ttype not in sql_tokens.Comment |
| and not ( |
| token.ttype in sql_tokens.Punctuation and str(token.value).strip() == ";" |
| ) |
| for token in statement.flatten() |
| ) |
| |
| |
| def _optional_identifier(value: Any) -> str | None: |
| if not isinstance(value, str) or not value.strip(): |
| return None |
| return value.strip() |
| |
| |
| def _result_warnings(result: Mapping[str, Any]) -> list[str]: |
| warnings = result.get("warnings") |
| if not isinstance(warnings, list | tuple): |
| return [] |
| return [str(warning) for warning in warnings if str(warning).strip()] |
| |
| |
| def _read_only_failure(message: str) -> QueryRuntimeFailure: |
| return QueryRuntimeFailure( |
| message, |
| reason_code="QUERY_READ_ONLY_VIOLATION", |
| status_code=400, |
| ) |
| |
| |
| def _validate_query_id(query_id: str) -> str: |
| if ( |
| not isinstance(query_id, str) |
| or len(query_id) > _MAX_QUERY_ID_LENGTH |
| or _QUERY_ID_RE.fullmatch(query_id.strip()) is None |
| ): |
| raise _argument_failure("Query ID is invalid.") |
| return query_id.strip() |
| |
| |
| def _bounded_integer( |
| value: Any, |
| name: str, |
| *, |
| minimum: int, |
| maximum: int, |
| ) -> int: |
| if isinstance(value, bool) or not isinstance(value, int): |
| raise _argument_failure(f"{name} must be an integer.") |
| if not minimum <= value <= maximum: |
| raise _argument_failure(f"{name} must be between {minimum} and {maximum}.") |
| return int(value) |
| |
| |
| def _context_statements( |
| catalog: str | None, |
| database: str | None, |
| ) -> tuple[str, ...]: |
| statements: list[str] = [] |
| try: |
| if catalog is not None: |
| validate_identifier(catalog, "catalog name") |
| statements.append(f"SWITCH {quote_identifier(catalog, 'catalog name')}") |
| if database is not None: |
| validate_identifier(database, "database name") |
| statements.append(f"USE {quote_identifier(database, 'database name')}") |
| except SQLSecurityError as exc: |
| raise _argument_failure("Doris query context is invalid.") from exc |
| return tuple(statements) |
| |
| |
| def _query_result( |
| result: QueryResult, |
| *, |
| limits: ResultLimits, |
| protocol: str, |
| operation: str, |
| ) -> dict[str, Any]: |
| rows = [ |
| _json_mapping(row) for row in (result.data or []) if isinstance(row, Mapping) |
| ] |
| raw_columns = result.metadata.get("columns", []) |
| columns = [ |
| column if isinstance(column, dict) else {"name": str(column)} |
| for column in ( |
| raw_columns |
| if isinstance(raw_columns, list) |
| else list(rows[0]) |
| if rows |
| else [] |
| ) |
| ] |
| truncated = bool(result.metadata.get("truncated")) |
| return _query_output( |
| columns=columns, |
| rows=rows, |
| truncated=truncated, |
| metadata={ |
| "source": "doris_query", |
| "protocol": protocol, |
| "operation": operation, |
| "execution_time_seconds": result.execution_time, |
| "result_bytes": result.metadata.get("result_bytes"), |
| "truncation_reason": result.metadata.get("truncation_reason"), |
| "limits": { |
| "max_rows": limits.max_rows, |
| "max_bytes": limits.max_bytes, |
| "timeout_seconds": limits.timeout_seconds, |
| }, |
| }, |
| ) |
| |
| |
| def _query_output( |
| *, |
| columns: list[dict[str, Any]], |
| rows: list[dict[str, Any]], |
| truncated: bool, |
| warnings: list[str] | tuple[str, ...] = (), |
| metadata: Mapping[str, Any] | None = None, |
| ) -> dict[str, Any]: |
| warning_list = list(warnings) |
| return { |
| "status": "partial" if warning_list or truncated else "success", |
| "data": { |
| "columns": columns, |
| "rows": rows, |
| "row_count": len(rows), |
| "truncated": truncated, |
| }, |
| "warnings": warning_list, |
| "metadata": dict(metadata or {}), |
| } |
| |
| |
| def _detail_result( |
| data: Mapping[str, Any], |
| *, |
| status: str = "success", |
| warnings: list[str] | tuple[str, ...] = (), |
| metadata: Mapping[str, Any] | None = None, |
| ) -> dict[str, Any]: |
| return { |
| "status": status, |
| "data": dict(data), |
| "warnings": list(warnings), |
| "metadata": dict(metadata or {}), |
| } |
| |
| |
| def _diagnostic_result( |
| data: Mapping[str, Any], |
| *, |
| status: str = "success", |
| warnings: list[str] | tuple[str, ...] = (), |
| metadata: Mapping[str, Any] | None = None, |
| evidence: tuple[Mapping[str, Any], ...] = (), |
| ) -> dict[str, Any]: |
| return { |
| **_detail_result( |
| data, |
| status=status, |
| warnings=warnings, |
| metadata=metadata, |
| ), |
| "evidence": [dict(item) for item in evidence], |
| } |
| |
| |
| def _collection_result( |
| items: list[dict[str, Any]], |
| *, |
| truncated: bool, |
| metadata: Mapping[str, Any], |
| ) -> dict[str, Any]: |
| return { |
| "status": "partial" if truncated else "success", |
| "data": { |
| "items": items, |
| "next_cursor": None, |
| "truncated": truncated, |
| }, |
| "warnings": ( |
| ["Additional slow-query records matched the bounded request."] |
| if truncated |
| else [] |
| ), |
| "metadata": dict(metadata), |
| } |
| |
| |
| def _json_mapping(value: Mapping[str, Any]) -> dict[str, Any]: |
| return {str(key): _json_value(item) for key, item in value.items()} |
| |
| |
| def _json_value(value: Any) -> Any: |
| if value is None or isinstance(value, str | int | float | bool): |
| return value |
| if isinstance(value, Decimal): |
| return str(value) |
| if isinstance(value, datetime): |
| return value.isoformat() |
| if isinstance(value, bytes): |
| return value.decode("utf-8", errors="replace") |
| if isinstance(value, Mapping): |
| return _json_mapping(value) |
| if isinstance(value, list | tuple): |
| return [_json_value(item) for item in value] |
| return str(value) |
| |
| |
| def _slow_query_item(row: Mapping[str, Any]) -> dict[str, Any]: |
| normalized = {str(key).lower(): value for key, value in row.items()} |
| sql = str(normalized.get("stmt") or "") |
| return { |
| "query_id": _json_value(normalized.get("query_id")), |
| "time": _json_value(normalized.get("time")), |
| "user": _json_value(normalized.get("user")), |
| "catalog": _json_value(normalized.get("catalog")), |
| "database": _json_value(normalized.get("db")), |
| "state": _json_value(normalized.get("state")), |
| "duration_ms": _optional_int(normalized.get("query_time")), |
| "cpu_time_ms": _optional_int(normalized.get("cpu_time_ms")), |
| "scan_bytes": _optional_int(normalized.get("scan_bytes")), |
| "scan_rows": _optional_int(normalized.get("scan_rows")), |
| "return_rows": _optional_int(normalized.get("return_rows")), |
| "peak_memory_bytes": _optional_int(normalized.get("peak_memory_bytes")), |
| "sql_hash": _json_value(normalized.get("sql_hash")), |
| "sql_digest": _json_value(normalized.get("sql_digest")), |
| "sql": sql[:_MAX_SLOW_QUERY_TEXT], |
| "sql_truncated": len(sql) > _MAX_SLOW_QUERY_TEXT, |
| "statement_fingerprint": hashlib.sha256(sql.encode("utf-8")).hexdigest()[:16], |
| } |
| |
| |
| def _optional_int(value: Any) -> int | None: |
| if value in (None, ""): |
| return None |
| try: |
| return int(value) |
| except (TypeError, ValueError): |
| return None |
| |
| |
| def _explain_facets(plan_text: str) -> dict[str, Any]: |
| upper = plan_text.upper() |
| return { |
| "has_join": "JOIN" in upper, |
| "has_cross_join": "CROSS JOIN" in upper, |
| "has_sort": "SORT" in upper or "ORDER BY" in upper, |
| "has_exchange": "EXCHANGE" in upper, |
| "has_runtime_filter": "RUNTIME FILTER" in upper, |
| "has_partition_pruning": ("PARTITION" in upper and "PRUN" in upper), |
| "mentions_inverted_index": "INVERTED" in upper, |
| "mentions_ann_index": "ANN" in upper or "HNSW" in upper, |
| } |
| |
| |
| def _profile_operator_tree(profile_text: str) -> list[str]: |
| lines = [] |
| for raw_line in profile_text.splitlines(): |
| line = raw_line.rstrip() |
| upper = line.upper() |
| if any( |
| marker in upper |
| for marker in ( |
| "SCAN", |
| "JOIN", |
| "AGGREGATE", |
| "EXCHANGE", |
| "SORT", |
| "SINK", |
| ) |
| ): |
| lines.append(line[:512]) |
| if len(lines) >= 256: |
| break |
| return lines |
| |
| |
| def _profile_facets(profile_text: str) -> dict[str, Any]: |
| upper = profile_text.upper() |
| return { |
| "mentions_spill": "SPILL" in upper, |
| "mentions_shuffle": "SHUFFLE" in upper or "EXCHANGE" in upper, |
| "mentions_runtime_filter": "RUNTIME FILTER" in upper, |
| "mentions_cache": "CACHE" in upper, |
| "mentions_peak_memory": "PEAKMEMORY" in upper or "PEAK MEMORY" in upper, |
| } |
| |
| |
| def _diagnostic_findings( |
| *, |
| explain_text: str, |
| profile_text: str, |
| audit_record: Mapping[str, Any] | None, |
| ) -> list[dict[str, Any]]: |
| findings: list[dict[str, Any]] = [] |
| upper_plan = explain_text.upper() |
| upper_profile = profile_text.upper() |
| if "CROSS JOIN" in upper_plan: |
| findings.append( |
| { |
| "code": "CROSS_JOIN_OBSERVED", |
| "severity": "high", |
| "evidence": "The Doris explain plan contains CROSS JOIN.", |
| } |
| ) |
| if "SPILL" in upper_profile and not re.search( |
| r"SPILL[^\n]*[:=]\s*0(?:\D|$)", |
| upper_profile, |
| ): |
| findings.append( |
| { |
| "code": "PROFILE_SPILL_OBSERVED", |
| "severity": "medium", |
| "evidence": "The Doris profile reports spill-related operators.", |
| } |
| ) |
| if audit_record is not None: |
| scan_rows = _optional_int(audit_record.get("scan_rows")) or 0 |
| return_rows = _optional_int(audit_record.get("return_rows")) or 0 |
| duration = _optional_int(audit_record.get("duration_ms")) or 0 |
| peak_memory = _optional_int(audit_record.get("peak_memory_bytes")) or 0 |
| if scan_rows >= 1_000_000 and scan_rows > max(return_rows, 1) * 100: |
| findings.append( |
| { |
| "code": "HIGH_SCAN_TO_RETURN_RATIO", |
| "severity": "medium", |
| "evidence": { |
| "scan_rows": scan_rows, |
| "return_rows": return_rows, |
| }, |
| } |
| ) |
| if duration >= 10_000: |
| findings.append( |
| { |
| "code": "LONG_RECORDED_DURATION", |
| "severity": "medium", |
| "evidence": {"duration_ms": duration}, |
| } |
| ) |
| if peak_memory >= 1024**3: |
| findings.append( |
| { |
| "code": "HIGH_PEAK_MEMORY", |
| "severity": "medium", |
| "evidence": {"peak_memory_bytes": peak_memory}, |
| } |
| ) |
| return findings |
| |
| |
| def _diagnostic_recommendations( |
| findings: list[dict[str, Any]], |
| ) -> list[dict[str, str]]: |
| recommendations = { |
| "CROSS_JOIN_OBSERVED": ( |
| "Review join predicates and verify that the Cartesian join is intentional." |
| ), |
| "PROFILE_SPILL_OBSERVED": ( |
| "Inspect high-cardinality joins, aggregations, and memory limits " |
| "for the spilling operator." |
| ), |
| "HIGH_SCAN_TO_RETURN_RATIO": ( |
| "Review partition pruning, predicate pushdown, and available indexes." |
| ), |
| "LONG_RECORDED_DURATION": ( |
| "Compare operator time in the profile with scan and shuffle evidence." |
| ), |
| "HIGH_PEAK_MEMORY": ( |
| "Inspect operator cardinality estimates and memory-intensive joins " |
| "or aggregations." |
| ), |
| } |
| return [ |
| { |
| "finding_code": str(finding["code"]), |
| "recommendation": recommendations[str(finding["code"])], |
| } |
| for finding in findings |
| if str(finding.get("code")) in recommendations |
| ] |
| |
| |
| def _profile_text_limit(config: Any) -> int: |
| performance = getattr(config, "performance", None) |
| value = getattr(performance, "max_response_content_size", 4096) |
| if isinstance(value, bool) or not isinstance(value, int) or value <= 0: |
| return 4096 |
| return min(value, 1024 * 1024) |
| |
| |
| def _http_json(response: DorisHTTPResponse) -> Mapping[str, Any]: |
| try: |
| payload = json.loads(response.text()) |
| except json.JSONDecodeError as exc: |
| raise QueryRuntimeFailure( |
| "Doris query profile service returned an invalid response.", |
| reason_code="QUERY_PROFILE_INVALID_RESPONSE", |
| status_code=502, |
| ) from exc |
| if not isinstance(payload, Mapping): |
| raise QueryRuntimeFailure( |
| "Doris query profile service returned an invalid response.", |
| reason_code="QUERY_PROFILE_INVALID_RESPONSE", |
| status_code=502, |
| ) |
| return payload |
| |
| |
| def _http_json_if_present( |
| response: DorisHTTPResponse, |
| ) -> Mapping[str, Any] | None: |
| content_type = response.headers.get("content-type", "") |
| text = response.text() |
| if "json" not in content_type.lower() and not text.lstrip().startswith("{"): |
| return None |
| return _http_json(response) |
| |
| |
| def _api_code(payload: Mapping[str, Any]) -> int | None: |
| value = payload.get("code") |
| if value is None: |
| return None |
| try: |
| return int(value) |
| except (TypeError, ValueError): |
| return -1 |
| |
| |
| def _profile_http_failure(status: int) -> QueryRuntimeFailure: |
| if status in {401, 403}: |
| return QueryRuntimeFailure( |
| "Doris denied access to query profile evidence.", |
| reason_code="QUERY_PROFILE_PERMISSION_DENIED", |
| status_code=403, |
| ) |
| return QueryRuntimeFailure( |
| "Doris query profile service is temporarily unavailable.", |
| reason_code="QUERY_PROFILE_BACKEND_UNAVAILABLE", |
| status_code=503, |
| retryable=True, |
| ) |
| |
| |
| def _require_explicit_adbc_intent(explicit_adbc: bool) -> None: |
| if explicit_adbc is not True: |
| raise QueryRuntimeFailure( |
| "ADBC requires an explicit end-user request for ADBC or Arrow " |
| "Flight SQL; use doris_query.execute_query for normal queries.", |
| reason_code="ADBC_EXPLICIT_USER_INTENT_REQUIRED", |
| status_code=400, |
| ) |
| |
| |
| def _classify_adbc_failure(raw: Any) -> QueryRuntimeFailure: |
| error_type = str(raw.get("error_type", "")) if isinstance(raw, Mapping) else "" |
| if error_type in { |
| "invalid_result_limits", |
| "security_violation", |
| }: |
| return QueryRuntimeFailure( |
| "ADBC rejected the query request.", |
| reason_code=( |
| "QUERY_READ_ONLY_VIOLATION" |
| if error_type == "security_violation" |
| else "QUERY_ARGUMENT_INVALID" |
| ), |
| status_code=400, |
| ) |
| if error_type == "timeout": |
| return QueryRuntimeFailure( |
| "ADBC query execution timed out.", |
| reason_code="QUERY_TIMEOUT", |
| status_code=504, |
| retryable=True, |
| ) |
| if error_type in { |
| "missing_fe_port_config", |
| "missing_be_port_config", |
| "missing_adbc_manager", |
| "missing_flight_sql_driver", |
| "token_bound_adbc_unsupported", |
| }: |
| return QueryRuntimeFailure( |
| "ADBC is not configured for the active Doris route.", |
| reason_code="ADBC_PROVIDER_NOT_CONFIGURED", |
| status_code=503, |
| ) |
| return QueryRuntimeFailure( |
| "ADBC query execution failed.", |
| reason_code="ADBC_EXECUTION_FAILED", |
| status_code=502, |
| ) |
| |
| |
| def _classify_query_failure(exc: Exception) -> QueryRuntimeFailure: |
| if isinstance(exc, QueryRuntimeFailure): |
| return exc |
| if isinstance(exc, SQLSecurityError | ResultLimitError): |
| return _argument_failure("Query arguments are invalid.") |
| numeric_code = next( |
| (value for value in getattr(exc, "args", ()) if isinstance(value, int)), |
| None, |
| ) |
| message = str(exc).lower() |
| if numeric_code in {1044, 1045, 1142, 1227} or any( |
| marker in message |
| for marker in ( |
| "access denied", |
| "permission denied", |
| "not authorized", |
| "privilege", |
| ) |
| ): |
| return QueryRuntimeFailure( |
| "Doris denied the query on the active route.", |
| reason_code="QUERY_PERMISSION_DENIED", |
| status_code=403, |
| ) |
| if numeric_code in {1049, 1054, 1146} or any( |
| marker in message |
| for marker in ( |
| "doesn't exist", |
| "does not exist", |
| "unknown column", |
| "unknown database", |
| "unknown table", |
| ) |
| ): |
| return QueryRuntimeFailure( |
| "A referenced Doris query object was not found.", |
| reason_code="QUERY_OBJECT_NOT_FOUND", |
| status_code=404, |
| ) |
| if numeric_code == 1064 or "syntax error" in message: |
| return QueryRuntimeFailure( |
| "Doris rejected the SQL syntax.", |
| reason_code="QUERY_SYNTAX_ERROR", |
| status_code=400, |
| ) |
| if isinstance(exc, TimeoutError | asyncio.TimeoutError): |
| return QueryRuntimeFailure( |
| "Doris query execution timed out.", |
| reason_code="QUERY_TIMEOUT", |
| status_code=504, |
| retryable=True, |
| ) |
| if isinstance(exc, ConnectionError | OSError) or any( |
| marker in message |
| for marker in ( |
| "connection reset", |
| "broken pipe", |
| "server has gone away", |
| "lost connection", |
| ) |
| ): |
| return QueryRuntimeFailure( |
| "Doris query execution is temporarily unavailable.", |
| reason_code="QUERY_BACKEND_UNAVAILABLE", |
| status_code=503, |
| retryable=True, |
| ) |
| return QueryRuntimeFailure( |
| "Doris query execution failed.", |
| reason_code="QUERY_EXECUTION_FAILED", |
| status_code=502, |
| ) |
| |
| |
| __all__ = [ |
| "DorisQueryRuntime", |
| "QueryRuntimeFailure", |
| "ReadOnlySQLGuard", |
| "ReadOnlyStatement", |
| ] |