From 53ea2d854d4e508841d9765eb020d020e91f7d79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E8=AE=A9=E5=AE=87?= Date: Fri, 11 Sep 2026 01:52:35 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20list/search=20quickSearch=20=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=90=8D=E4=BF=AE=E6=AD=A3=20+=20SQL=20=E6=96=B9?= =?UTF-8?q?=E8=A8=80=E5=8F=8B=E5=A5=BD=E6=8A=A5=E9=94=99=20+=20E071=20?= =?UTF-8?q?=E4=BC=A0=E8=BE=93=E8=AF=B7=E6=B1=82=E6=94=B9=20IN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - list_objects: maxrow/name/type 改为 operation=quickSearch/query/maxResults/objectType, 并剥离 NW 7.40/7.50 quickSearch name 里的本地化类型标签(如 "ZCL_FOO (Class)") - query_table_data: HTTP 400 转抛 SapCliError,说明方言限制(WHERE 仅 IN/LIKE),不再打堆栈 - _query_transport_request: E071 查询 WHERE 改用 IN(...),区分「无」与「无法获取」 --- assets/sapcli/client/_ddic.py | 8 +++++++- assets/sapcli/client/_search.py | 22 ++++++++++++--------- assets/sapcli/commands/crud.py | 4 ++-- tests/unit/test_client.py | 25 ++++++++++++++++++++++++ tests/unit/test_ddic.py | 10 +++++++++- tests/unit/test_info_ddic_enhancement.py | 13 ++++++++++++ 6 files changed, 69 insertions(+), 13 deletions(-) diff --git a/assets/sapcli/client/_ddic.py b/assets/sapcli/client/_ddic.py index 63da604..627820b 100644 --- a/assets/sapcli/client/_ddic.py +++ b/assets/sapcli/client/_ddic.py @@ -6,7 +6,7 @@ import logging import xml.etree.ElementTree as ET from typing import Any -from sapcli.exceptions import CreateError, DeleteError +from sapcli.exceptions import CreateError, DeleteError, SapCliError from sapcli.types import ObjectTypeConfig, get_type_config logger = logging.getLogger("sapcli.client") @@ -839,6 +839,12 @@ class DdicMixin: logger.info("QUERY TABLE DATA: POST %s sql=%s maxRows=%d", url, sql[:80], max_rows) resp = self.session.post(url, headers=hdrs, params=params, data=sql.encode("utf-8")) logger.info("QUERY TABLE DATA RESPONSE: HTTP %s", resp.status_code) + if resp.status_code == 400: + raise SapCliError( + "ADT Data Preview 拒绝该 SQL(HTTP 400)。本系统 SQL 方言限制:" + "WHERE 只支持 IN(...) / LIKE '...',不支持 '=';SELECT 列表必须逗号分隔。" + "请改用 IN / LIKE 重试。" + ) resp.raise_for_status() root = ET.fromstring(resp.content) diff --git a/assets/sapcli/client/_search.py b/assets/sapcli/client/_search.py index 80e86b2..937083e 100644 --- a/assets/sapcli/client/_search.py +++ b/assets/sapcli/client/_search.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import re import xml.etree.ElementTree as ET from sapcli.exceptions import SapCliError @@ -10,6 +11,11 @@ from sapcli.exceptions import SapCliError logger = logging.getLogger("sapcli.client") +def _strip_type_label(name: str) -> str: + """剥离 NW 7.40/7.50 quickSearch 名称里的本地化类型标签(如 ``ZCL_FOO (Class)``)。""" + return re.sub(r"\s*\(.*$", "", name).strip() + + class SearchMixin: """Object search, where-used listing, and source-code search.""" @@ -30,15 +36,13 @@ class SearchMixin: 列表,每项含 ``name``, ``type``, ``description``, ``package``。 """ url = f"{self.host}/sap/bc/adt/repository/informationsystem/search" - params: dict[str, str] = {"maxrow": "200"} + params: dict[str, str] = { + "operation": "quickSearch", + "query": prefix or "*", + "maxResults": "200", + } if obj_type: - params["type"] = obj_type - if prefix: - params["name"] = prefix - else: - params["name"] = "*" - if package: - params["pkg"] = package + params["objectType"] = obj_type hdrs = self._headers() hdrs["Accept"] = "application/xml" @@ -51,7 +55,7 @@ class SearchMixin: results: list[dict[str, str]] = [] core_ns = "http://www.sap.com/adt/core" for ref in root.findall(f".//{{{core_ns}}}objectReference"): - name = ref.attrib.get(f"{{{core_ns}}}name", "") + name = _strip_type_label(ref.attrib.get(f"{{{core_ns}}}name", "")) otype = ref.attrib.get(f"{{{core_ns}}}type", "") desc = ref.attrib.get(f"{{{core_ns}}}description", "") pkg = ref.attrib.get(f"{{{core_ns}}}package", "") diff --git a/assets/sapcli/commands/crud.py b/assets/sapcli/commands/crud.py index dd24266..152befb 100644 --- a/assets/sapcli/commands/crud.py +++ b/assets/sapcli/commands/crud.py @@ -443,8 +443,8 @@ def _query_transport_request( return "(对象名非法,跳过)" try: sql = ( - f"SELECT trkorr FROM e071 WHERE pgmid='R3TR' " - f"AND object='{r3tr_code}' AND obj_name='{obj_name_upper}' " + f"SELECT trkorr FROM e071 WHERE pgmid IN ('R3TR') " + f"AND object IN ('{r3tr_code}') AND obj_name IN ('{obj_name_upper}') " f"UP TO 10 ROWS" ) result = client.query_table_data(sql, max_rows=10) diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 5a8e2cd..2a79522 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -646,6 +646,31 @@ class TestListObjects(unittest.TestCase): client.session.get.return_value = _mock_resp(200, content=_LIST_EMPTY) self.assertEqual(client.list_objects(), []) + def test_quickssearch_params(self): + client = _make_client() + client.session.get.return_value = _mock_resp(200, content=_LIST_TWO) + client.list_objects(obj_type="PROG/P", prefix="Z_TEST") + _, kwargs = client.session.get.call_args + params = kwargs["params"] + self.assertEqual(params["operation"], "quickSearch") + self.assertEqual(params["query"], "Z_TEST") + self.assertEqual(params["maxResults"], "200") + self.assertEqual(params["objectType"], "PROG/P") + for bad in ("maxrow", "name", "type"): + self.assertNotIn(bad, params) + + def test_strips_type_label_from_name(self): + client = _make_client() + xml = ( + f'' + f'' + f'' + f'' + ).encode() + client.session.get.return_value = _mock_resp(200, content=xml) + result = client.list_objects() + self.assertEqual(result[0]["name"], "ZMM_BIP_001_HEADER") + class TestWhereUsed(unittest.TestCase): diff --git a/tests/unit/test_ddic.py b/tests/unit/test_ddic.py index 729afab..5c36311 100644 --- a/tests/unit/test_ddic.py +++ b/tests/unit/test_ddic.py @@ -19,7 +19,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".." from sapcli.client import ADTClient from sapcli.client._ddic import _local, _attr_local, _find_local -from sapcli.exceptions import CreateError, DeleteError +from sapcli.exceptions import CreateError, DeleteError, SapCliError from sapcli.types import parse_object_name # ADT XML 命名空间 @@ -706,6 +706,14 @@ class TestQueryTableData(unittest.TestCase): self.assertEqual(result["rows"], []) self.assertEqual(result["total_rows"], 0) + def test_http_400_raises_friendly_error(self): + client = _make_client() + client.session.post.return_value = _mock_resp(400, text="A Boolean expression was expected") + with self.assertRaises(SapCliError) as ctx: + client.query_table_data("SELECT * FROM t WHERE f='x'") + self.assertIn("IN", str(ctx.exception)) + self.assertIn("LIKE", str(ctx.exception)) + # ═══════════════════════════════════════════ # run_program diff --git a/tests/unit/test_info_ddic_enhancement.py b/tests/unit/test_info_ddic_enhancement.py index 7103549..fca238d 100644 --- a/tests/unit/test_info_ddic_enhancement.py +++ b/tests/unit/test_info_ddic_enhancement.py @@ -218,5 +218,18 @@ class TestCmdInfoSupplement(unittest.TestCase): self.assertIn("(未指定)", printed) +class TestTransportRequestSql(unittest.TestCase): + def test_uses_in_not_equals(self): + from sapcli.commands.crud import _query_transport_request + client = _make_client() + client.query_table_data.return_value = {"rows": []} + _query_transport_request(client, "class", "ZCL_MM_BIP_TYPES") + sql = client.query_table_data.call_args[0][0] + self.assertIn("IN ('R3TR')", sql) + self.assertIn("IN ('CLAS')", sql) + self.assertIn("IN ('ZCL_MM_BIP_TYPES')", sql) + self.assertNotIn("=", sql) + + if __name__ == "__main__": unittest.main()