fix: list/search quickSearch 参数名修正 + SQL 方言友好报错 + E071 传输请求改 IN

- 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(...),区分「无」与「无法获取」
This commit is contained in:
吴让宇
2026-09-11 01:52:35 +08:00
parent d0d65db137
commit 53ea2d854d
6 changed files with 69 additions and 13 deletions
+7 -1
View File
@@ -6,7 +6,7 @@ import logging
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from typing import Any 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 from sapcli.types import ObjectTypeConfig, get_type_config
logger = logging.getLogger("sapcli.client") 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) 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")) 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) logger.info("QUERY TABLE DATA RESPONSE: HTTP %s", resp.status_code)
if resp.status_code == 400:
raise SapCliError(
"ADT Data Preview 拒绝该 SQLHTTP 400)。本系统 SQL 方言限制:"
"WHERE 只支持 IN(...) / LIKE '...',不支持 '='SELECT 列表必须逗号分隔。"
"请改用 IN / LIKE 重试。"
)
resp.raise_for_status() resp.raise_for_status()
root = ET.fromstring(resp.content) root = ET.fromstring(resp.content)
+13 -9
View File
@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import re
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from sapcli.exceptions import SapCliError from sapcli.exceptions import SapCliError
@@ -10,6 +11,11 @@ from sapcli.exceptions import SapCliError
logger = logging.getLogger("sapcli.client") 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: class SearchMixin:
"""Object search, where-used listing, and source-code search.""" """Object search, where-used listing, and source-code search."""
@@ -30,15 +36,13 @@ class SearchMixin:
列表,每项含 ``name``, ``type``, ``description``, ``package``。 列表,每项含 ``name``, ``type``, ``description``, ``package``。
""" """
url = f"{self.host}/sap/bc/adt/repository/informationsystem/search" 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: if obj_type:
params["type"] = obj_type params["objectType"] = obj_type
if prefix:
params["name"] = prefix
else:
params["name"] = "*"
if package:
params["pkg"] = package
hdrs = self._headers() hdrs = self._headers()
hdrs["Accept"] = "application/xml" hdrs["Accept"] = "application/xml"
@@ -51,7 +55,7 @@ class SearchMixin:
results: list[dict[str, str]] = [] results: list[dict[str, str]] = []
core_ns = "http://www.sap.com/adt/core" core_ns = "http://www.sap.com/adt/core"
for ref in root.findall(f".//{{{core_ns}}}objectReference"): 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", "") otype = ref.attrib.get(f"{{{core_ns}}}type", "")
desc = ref.attrib.get(f"{{{core_ns}}}description", "") desc = ref.attrib.get(f"{{{core_ns}}}description", "")
pkg = ref.attrib.get(f"{{{core_ns}}}package", "") pkg = ref.attrib.get(f"{{{core_ns}}}package", "")
+2 -2
View File
@@ -443,8 +443,8 @@ def _query_transport_request(
return "(对象名非法,跳过)" return "(对象名非法,跳过)"
try: try:
sql = ( sql = (
f"SELECT trkorr FROM e071 WHERE pgmid='R3TR' " f"SELECT trkorr FROM e071 WHERE pgmid IN ('R3TR') "
f"AND object='{r3tr_code}' AND obj_name='{obj_name_upper}' " f"AND object IN ('{r3tr_code}') AND obj_name IN ('{obj_name_upper}') "
f"UP TO 10 ROWS" f"UP TO 10 ROWS"
) )
result = client.query_table_data(sql, max_rows=10) result = client.query_table_data(sql, max_rows=10)
+25
View File
@@ -646,6 +646,31 @@ class TestListObjects(unittest.TestCase):
client.session.get.return_value = _mock_resp(200, content=_LIST_EMPTY) client.session.get.return_value = _mock_resp(200, content=_LIST_EMPTY)
self.assertEqual(client.list_objects(), []) 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'<?xml version="1.0"?>'
f'<adtcore:objectReferences xmlns:adtcore="{ADTCORE_NS}">'
f'<adtcore:objectReference adtcore:name="ZMM_BIP_001_HEADER (Structure)" adtcore:type="TABL/ST"/>'
f'</adtcore:objectReferences>'
).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): class TestWhereUsed(unittest.TestCase):
+9 -1
View File
@@ -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 import ADTClient
from sapcli.client._ddic import _local, _attr_local, _find_local 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 from sapcli.types import parse_object_name
# ADT XML 命名空间 # ADT XML 命名空间
@@ -706,6 +706,14 @@ class TestQueryTableData(unittest.TestCase):
self.assertEqual(result["rows"], []) self.assertEqual(result["rows"], [])
self.assertEqual(result["total_rows"], 0) 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 # run_program
+13
View File
@@ -218,5 +218,18 @@ class TestCmdInfoSupplement(unittest.TestCase):
self.assertIn("(未指定)", printed) 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__": if __name__ == "__main__":
unittest.main() unittest.main()