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:
@@ -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)
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'<?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):
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user