This is an automated email from the ASF dual-hosted git repository. FreeOnePlus pushed a commit to branch feat/v1.0.0-hierarchical-tools in repository https://gitbox.apache.org/repos/asf/doris-mcp-server.git
commit 984288e5218ff772f3ba498daf7271efa63df8fd Author: FreeOnePlus <[email protected]> AuthorDate: Fri Jul 31 00:40:39 2026 +0800 feat: add fail-closed Doris version probing --- doris_mcp_server/tools/doris_version.py | 195 ++++++++++++++++++++++++++++++++ test/tools/test_doris_version.py | 170 ++++++++++++++++++++++++++++ 2 files changed, 365 insertions(+) diff --git a/doris_mcp_server/tools/doris_version.py b/doris_mcp_server/tools/doris_version.py new file mode 100644 index 0000000..f8aef78 --- /dev/null +++ b/doris_mcp_server/tools/doris_version.py @@ -0,0 +1,195 @@ +# 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. + +"""Fail-closed Apache Doris version probing, parsing, and comparison.""" + +from __future__ import annotations + +import re +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from enum import StrEnum +from typing import Any + +from ..utils.db import DorisConnection + +DORIS_VERSION_COMMENT_QUERY = "SELECT @@version_comment;" +_VERSION_COMMENT_COLUMN = "@@version_comment" +_VERSION_COMMENT_MAX_BYTES = 4096 + +_VERSION_PATTERN = re.compile( + r""" + (?<![A-Za-z0-9_]) + (?:apache\s+)?doris + (?:\s*,?\s*version)? + (?:\s+doris-|\s*-\s*|\s+) + (?P<core>\d+\.\d+\.\d+) + (?:-(?P<prerelease>rc\d+|alpha\d*|beta\d*))? + (?:-(?P<commit>[0-9a-f]{7,40}))? + (?=\s|\(|,|$) + """, + re.IGNORECASE | re.VERBOSE, +) +_PRERELEASE_PATTERN = re.compile( + r"(?P<kind>alpha|beta|rc)(?P<number>\d*)", + re.IGNORECASE, +) +_DEPLOYMENT_PATTERNS = ( + ("cloud", re.compile(r"\bcloud\s+mode\b", re.IGNORECASE)), + ("shared_data", re.compile(r"\bshared[-\s]+data\b", re.IGNORECASE)), + ("shared_nothing", re.compile(r"\bshared[-\s]+nothing\b", re.IGNORECASE)), +) +_PRERELEASE_RANK = {"alpha": 0, "beta": 1, "rc": 2} +_NORMALIZED_PRERELEASE = {"alpha": "a", "beta": "b", "rc": "rc"} + + +class DorisVersionParseStatus(StrEnum): + PARSED = "parsed" + UNKNOWN = "unknown" + + +@dataclass(frozen=True, slots=True) +class DorisVersion: + raw: str + major: int | None = None + minor: int | None = None + patch: int | None = None + prerelease: str | None = None + commit: str | None = None + deployment_hint: str | None = None + parse_status: DorisVersionParseStatus = DorisVersionParseStatus.UNKNOWN + + @property + def is_parsed(self) -> bool: + return self.parse_status is DorisVersionParseStatus.PARSED + + @property + def core(self) -> str | None: + if ( + not self.is_parsed + or self.major is None + or self.minor is None + or self.patch is None + ): + return None + return f"{self.major}.{self.minor}.{self.patch}" + + @property + def normalized(self) -> str | None: + core = self.core + if core is None or self.prerelease is None: + return core + + prerelease = _parse_prerelease(self.prerelease) + kind, number = prerelease + return f"{core}{_NORMALIZED_PRERELEASE[kind]}{number}" + + def compare(self, other: DorisVersion) -> int: + left = self._comparison_key() + right = other._comparison_key() + return (left > right) - (left < right) + + def is_at_least(self, other: DorisVersion) -> bool: + return self.compare(other) >= 0 + + def _comparison_key(self) -> tuple[int, int, int, int, int]: + if ( + not self.is_parsed + or self.major is None + or self.minor is None + or self.patch is None + ): + raise ValueError("Cannot compare an unparsed Doris version") + + if self.prerelease is None: + prerelease_rank = len(_PRERELEASE_RANK) + prerelease_number = 0 + else: + kind, prerelease_number = _parse_prerelease(self.prerelease) + prerelease_rank = _PRERELEASE_RANK[kind] + + return ( + self.major, + self.minor, + self.patch, + prerelease_rank, + prerelease_number, + ) + + +def parse_doris_version_comment(comment: str) -> DorisVersion: + deployment_hint = _detect_deployment_hint(comment) + match = _VERSION_PATTERN.search(comment) + if match is None: + return DorisVersion(raw=comment, deployment_hint=deployment_hint) + + major, minor, patch = (int(part) for part in match.group("core").split(".")) + prerelease = match.group("prerelease") + commit = match.group("commit") + + return DorisVersion( + raw=comment, + major=major, + minor=minor, + patch=patch, + prerelease=prerelease.lower() if prerelease else None, + commit=commit.lower() if commit else None, + deployment_hint=deployment_hint, + parse_status=DorisVersionParseStatus.PARSED, + ) + + +def parse_doris_version_rows( + rows: Sequence[Mapping[str, Any]], +) -> DorisVersion: + if not rows: + return DorisVersion(raw="") + + for key, value in rows[0].items(): + if key.strip().casefold() != _VERSION_COMMENT_COLUMN: + continue + if isinstance(value, str): + return parse_doris_version_comment(value) + return DorisVersion(raw="") + + return DorisVersion(raw="") + + +async def probe_doris_version(connection: DorisConnection) -> DorisVersion: + result = await connection.execute( + DORIS_VERSION_COMMENT_QUERY, + mask_result=False, + max_rows=1, + max_bytes=_VERSION_COMMENT_MAX_BYTES, + ) + return parse_doris_version_rows(result.data) + + +def _parse_prerelease(value: str) -> tuple[str, int]: + match = _PRERELEASE_PATTERN.fullmatch(value) + if match is None: + raise ValueError(f"Unsupported Doris prerelease: {value}") + kind = match.group("kind").lower() + number = int(match.group("number") or "0") + return kind, number + + +def _detect_deployment_hint(comment: str) -> str | None: + for name, pattern in _DEPLOYMENT_PATTERNS: + if pattern.search(comment): + return name + return None diff --git a/test/tools/test_doris_version.py b/test/tools/test_doris_version.py new file mode 100644 index 0000000..f349d13 --- /dev/null +++ b/test/tools/test_doris_version.py @@ -0,0 +1,170 @@ +# 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. + +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from doris_mcp_server.tools.doris_version import ( + DORIS_VERSION_COMMENT_QUERY, + DorisVersionParseStatus, + parse_doris_version_comment, + parse_doris_version_rows, + probe_doris_version, +) +from doris_mcp_server.utils.db import DorisConnection, QueryResult + + +def test_version_probe_uses_version_comment() -> None: + assert DORIS_VERSION_COMMENT_QUERY == "SELECT @@version_comment;" + + [email protected]( + ("comment", "core", "prerelease", "commit", "deployment_hint", "normalized"), + [ + ( + "Doris version doris-3.0.3-rc03-43f06a5e26 (Cloud Mode)", + "3.0.3", + "rc03", + "43f06a5e26", + "cloud", + "3.0.3rc3", + ), + ( + "Apache Doris version 4.0.7", + "4.0.7", + None, + None, + None, + "4.0.7", + ), + ( + "doris-4.1.3-abcdef1234", + "4.1.3", + None, + "abcdef1234", + None, + "4.1.3", + ), + ], +) +def test_parse_supported_version_comments( + comment: str, + core: str, + prerelease: str | None, + commit: str | None, + deployment_hint: str | None, + normalized: str, +) -> None: + version = parse_doris_version_comment(comment) + + assert version.parse_status is DorisVersionParseStatus.PARSED + assert version.core == core + assert version.prerelease == prerelease + assert version.commit == commit + assert version.deployment_hint == deployment_hint + assert version.normalized == normalized + assert version.raw == comment + + [email protected]( + "comment", + [ + "", + "MySQL 8.0.36", + "version 4.1.3", + "Doris version unknown", + "Doris version 4.1.3-preview1", + "Doris version 4.1", + ], +) +def test_unknown_version_comments_fail_closed(comment: str) -> None: + version = parse_doris_version_comment(comment) + + assert version.parse_status is DorisVersionParseStatus.UNKNOWN + assert version.is_parsed is False + assert version.core is None + assert version.normalized is None + + +def test_comparison_ignores_commit_and_orders_prereleases() -> None: + alpha = parse_doris_version_comment("Doris version 3.0.3-alpha1") + beta = parse_doris_version_comment("Doris version 3.0.3-beta2") + release_candidate = parse_doris_version_comment("Doris version 3.0.3-rc03") + stable_a = parse_doris_version_comment("Doris version 3.0.3-43f06a5e26") + stable_b = parse_doris_version_comment("Doris version 3.0.3-abcdef1234") + + assert beta.compare(alpha) > 0 + assert release_candidate.compare(beta) > 0 + assert stable_a.compare(release_candidate) > 0 + assert stable_a.compare(stable_b) == 0 + assert stable_a.is_at_least(release_candidate) is True + + +def test_unparsed_version_cannot_be_compared() -> None: + unknown = parse_doris_version_comment("Doris version unknown") + stable = parse_doris_version_comment("Doris version 4.1.3") + + with pytest.raises(ValueError, match="unparsed Doris version"): + unknown.compare(stable) + + [email protected]( + "rows", + [ + [], + [{"version_comment": "Doris version 4.1.3"}], + [{"@@version_comment": None}], + ], +) +def test_version_rows_fail_closed_without_expected_string_column( + rows: list[dict[str, object]], +) -> None: + version = parse_doris_version_rows(rows) + + assert version.parse_status is DorisVersionParseStatus.UNKNOWN + + +async def test_probe_executes_exact_read_only_query() -> None: + connection = MagicMock(spec=DorisConnection) + connection.execute = AsyncMock( + return_value=QueryResult( + data=[ + { + "@@version_comment": ( + "Doris version doris-3.0.3-rc03-43f06a5e26 " + "(Cloud Mode)" + ) + } + ], + metadata={}, + execution_time=0.01, + row_count=1, + sql=DORIS_VERSION_COMMENT_QUERY, + ) + ) + + version = await probe_doris_version(connection) + + assert version.normalized == "3.0.3rc3" + assert version.deployment_hint == "cloud" + connection.execute.assert_awaited_once_with( + DORIS_VERSION_COMMENT_QUERY, + mask_result=False, + max_rows=1, + max_bytes=4096, + ) --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
