This is an automated email from the ASF dual-hosted git repository.

Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git


The following commit(s) were added to refs/heads/master by this push:
     new bf2f583a8b Fixes #6806: Skip the plugin jar blob in list queries and 
index snapshot joins (#7360)
bf2f583a8b is described below

commit bf2f583a8bd6aca3f75a96600864bea5ea190d2a
Author: BobSong <[email protected]>
AuthorDate: Thu Oct 8 15:29:40 2026 +0800

    Fixes #6806: Skip the plugin jar blob in list queries and index snapshot 
joins (#7360)
    
    * perf(admin): index snapshot joins and drop the plugin jar from list 
queries
    
    * chore(ci): re-trigger e2e workflow after infra flake
    
    * chore(ci): re-trigger e2e and k8s examples workflows after infra flake
    
    * chore(ci): re-trigger workflows after upstream dependency resolution flake
    
    * chore(ci): retry checks after unrelated test and infrastructure failures
    
    * test(e2e): await RocketMQ logging rule synchronization
    
    * fix(admin): reuse plugin list columns and guard upgrade indexes
    
    * fix(admin): use metadata-only plugin list responses
    
    * ci: retry storage e2e after HTTPBin upstream failure
    
    * test(admin): cover plugin index upgrade contracts
    
    * test(ci): gate SQL compatibility on native database matrix
    
    * test(ci): handle native catalog and tenant readiness differences
    
    ---------
    
    Co-authored-by: BobSong-dev <[email protected]>
---
 .github/scripts/native-sql-matrix.py               | 266 ++++++++++++++++++++
 .github/scripts/test-native-sql-matrix.py          | 230 +++++++++++++++++
 .github/workflows/native-sql-matrix.yml            | 139 +++++++++++
 db/init/mysql/schema.sql                           |   6 +-
 db/init/ob/schema.sql                              |  13 +-
 db/init/og/create-table.sql                        |  21 ++
 db/init/oracle/schema.sql                          |   9 +
 db/init/pg/create-table.sql                        |  21 ++
 db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql           |   4 +
 db/upgrade/2.7.1-upgrade-2.7.2-ob.sql              |   7 +
 db/upgrade/2.7.1-upgrade-2.7.2-og.sql              |   7 +
 db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql          |   7 +
 db/upgrade/2.7.1-upgrade-2.7.2-pg.sql              |   7 +
 db/upgrade/native-sql-matrix.md                    |  70 ++++++
 .../shenyu/admin/controller/PluginController.java  |   7 +-
 .../apache/shenyu/admin/mapper/PluginMapper.java   |   4 +-
 .../apache/shenyu/admin/model/vo/PluginListVO.java | 277 +++++++++++++++++++++
 .../apache/shenyu/admin/service/PluginService.java |   5 +-
 .../admin/service/impl/PluginServiceImpl.java      |   7 +-
 .../src/main/resources/mappers/plugin-sqlmap.xml   |  12 +-
 .../src/main/resources/sql-script/h2/schema.sql    |   4 +
 .../admin/controller/PluginControllerTest.java     |  49 +++-
 .../shenyu/admin/mapper/AdminQueryIndexTest.java   |   2 +
 .../mapper/PluginIndexUpgradeContractTest.java     |  95 +++++++
 .../shenyu/admin/mapper/PluginMapperTest.java      |  49 ++++
 .../shenyu/admin/service/PluginServiceTest.java    |   3 +-
 26 files changed, 1295 insertions(+), 26 deletions(-)

diff --git a/.github/scripts/native-sql-matrix.py 
b/.github/scripts/native-sql-matrix.py
new file mode 100644
index 0000000000..5343953b0c
--- /dev/null
+++ b/.github/scripts/native-sql-matrix.py
@@ -0,0 +1,266 @@
+# 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.
+
+"""Execute fresh schemas and the complete released-2.7.1 upgrade on native 
engines."""
+
+import argparse
+import json
+import re
+import subprocess
+import time
+import uuid
+import xml.etree.ElementTree as ET
+from pathlib import Path
+
+BASELINE = "218c5634ebffb1f0ce7e8ea921b85cd5a633cde7"  # v2.7.1, peeled commit
+PASSWORD = "SqlMatrix@2026"
+SENTINEL = "sql-matrix-plugin"
+JAR_HEX = "0001027fff"
+DIALECTS = {
+    "mysql": 
("mysql:8.0@sha256:7dcddc01f13bab2f15cde676d44d01f61fc9f99fe7785e86196dfc07d358ae2b",
 "db/init/mysql/schema.sql"),
+    "ob": 
("oceanbase/oceanbase-ce:4.3.5-lts@sha256:31086a6900c21c479c2bcd942b6a28c53b17a51f4e9b9eb8eafcc596adfcd2e3",
 "db/init/ob/schema.sql"),
+    "pg": 
("postgres:15@sha256:724292da1f2e50bdccfc3302ce75bbba7f4a6076701b588cc795fcac65683550",
 "db/init/pg/create-table.sql"),
+    "og": 
("enmotech/opengauss-lite:5.0.1@sha256:b0e9d4e7452007cb0a216e39f1927c889178dac695176af7ab0eec43ce077af3",
 "db/init/og/create-table.sql"),
+    "oracle": 
("gvenzl/oracle-free:23.26.3-slim@sha256:6d61d267a3b978c24c5ac1790e62e927416a0aec446bd86e4b3a1527562757bd",
 "db/init/oracle/schema.sql"),
+}
+TABLES = ("plugin", "selector", "rule", "resource", "permission", "user_role", 
"namespace_plugin_rel")
+REQUIRED = {"selector": "plugin_id", "resource": "parent_id", "user_role": 
"user_id"}
+
+
+def run(command, data=None, timeout=180):
+    result = subprocess.run(command, input=data, stdout=subprocess.PIPE, 
stderr=subprocess.STDOUT,
+                            text=True, encoding="utf-8", errors="replace", 
timeout=timeout)
+    if result.returncode:
+        raise RuntimeError(f"Command failed (exit 
{result.returncode}):\n{result.stdout[-12000:]}")
+    return result.stdout
+
+
+def scalar(output):
+    lines = [line.strip() for line in output.splitlines() if line.strip()]
+    if len(lines) != 1:
+        raise AssertionError(f"Expected exactly one scalar result, got 
{lines!r}")
+    return lines[0]
+
+
+def fragment_columns(mapper):
+    document = ET.parse(mapper).getroot()
+    fragments = {node.attrib["id"]: node for node in document.findall("sql")}
+
+    def expand(name, active=()):
+        if name in active:
+            raise AssertionError("Recursive SQL fragment")
+        node = fragments[name]
+        text = node.text or ""
+        for child in node:
+            if child.tag != "include":
+                raise AssertionError("Unexpected dynamic column fragment")
+            text += expand(child.attrib["refid"], active + (name,)) + 
(child.tail or "")
+        return text
+
+    listing = [column.strip() for column in 
expand("List_Column_List").split(",")]
+    detail = [column.strip() for column in 
expand("Base_Column_List").split(",")]
+    if listing != ["id", "date_created", "date_updated", "name", "config", 
"role", "sort", "enabled"]:
+        raise AssertionError(f"Unexpected plugin list projection: {listing}")
+    if detail != listing + ["plugin_jar"]:
+        raise AssertionError("Detail/export must retain plugin_jar")
+    return listing, detail
+
+
+def verify_index_rows(rows, postgres=False):
+    indexes = {}
+    for line in rows.splitlines():
+        if not line.strip():
+            continue
+        table, name, value = line.strip().lower().split("|", 2)
+        if postgres:
+            match = re.search(r'\busing\s+(?:btree|ubtree)\s*\(([^()]*)\)'
+                              
r'(?:\s+tablespace\s+(?:"[^"]+"|[a-z_]\w*))?\s*$', value)
+            if not match:
+                continue  # expression indexes are not usable for these column 
lookups
+            indexes[(table, name)] = [column.strip().strip('"') for column in 
match[1].split(",")]
+        else:
+            column, position = value.split("|")
+            indexes.setdefault((table, name), []).append((int(position), 
column))
+    if not postgres:
+        indexes = {key: [column for _, column in sorted(values)] for key, 
values in indexes.items()}
+    for table, column in list(REQUIRED.items()) + [("permission", 
"object_id"), ("permission", "resource_id")]:
+        if not any(t == table and columns and columns[0] == column for (t, _), 
columns in indexes.items()):
+            raise AssertionError(f"Missing leading-column index: 
{table}({column})")
+    for table, name, column in [("selector", "idx_selector_plugin_id", 
"plugin_id"),
+                                ("permission", "idx_permission_resource_id", 
"resource_id")]:
+        if indexes.get((table, name)) != [column]:
+            raise AssertionError(f"Wrong index columns: {name}")
+    return indexes
+
+
+class Engine:
+    def __init__(self, dialect, flow, output):
+        self.dialect = dialect
+        self.name = f"shenyu-sql-{dialect}-{flow}-{uuid.uuid4().hex[:8]}"
+        self.output = output
+
+    def start(self):
+        image, _ = DIALECTS[self.dialect]
+        run(["docker", "pull", image], timeout=900)
+        environment = {
+            "mysql": ["MYSQL_ROOT_PASSWORD=" + PASSWORD],
+            "ob": ["MODE=mini", "OB_TENANT_NAME=test"],
+            "pg": ["POSTGRES_PASSWORD=" + PASSWORD, "POSTGRES_DB=shenyu"],
+            "og": ["GS_PASSWORD=" + PASSWORD, "GS_DB=shenyu"],
+            "oracle": ["ORACLE_PASSWORD=" + PASSWORD, "APP_USER=sqlmatrix", 
"APP_USER_PASSWORD=" + PASSWORD],
+        }[self.dialect]
+        command = ["docker", "run", "-d", "--name", self.name, "--shm-size=1g"]
+        for variable in environment:
+            command += ["-e", variable]
+        run(command + [image])
+        deadline = time.monotonic() + 900
+        last_error = ""
+        while time.monotonic() < deadline:
+            try:
+                query = "SELECT 1 FROM dual;" if self.dialect == "oracle" else 
"SELECT 1;"
+                if scalar(self.sql(query, database=False, timeout=20)) == "1":
+                    if self.dialect == "ob":
+                        # SELECT works before OceanBase finishes tenant 
creation. Probe
+                        # disposable DDL before executing either real schema, 
never retry
+                        # or suppress an error inside the actual 
schema/upgrade script.
+                        self.sql("CREATE DATABASE IF NOT EXISTS 
sql_matrix_ready; DROP DATABASE sql_matrix_ready;",
+                                 database=False, timeout=20)
+                    return
+            except (RuntimeError, subprocess.TimeoutExpired, AssertionError) 
as error:
+                last_error = str(error)
+            time.sleep(5)
+        raise RuntimeError("Database readiness timeout: " + last_error)
+
+    def sql(self, sql, database=True, timeout=180):
+        command = ["docker", "exec", "-i"]
+        if self.dialect == "og":
+            command += ["-e", "LD_LIBRARY_PATH=/usr/local/opengauss/lib"]
+        command += [self.name]
+        if self.dialect in ("mysql", "ob"):
+            client = "mysql" if self.dialect == "mysql" else "obclient"
+            user = "root" if self.dialect == "mysql" else "root@test"
+            command += [client, "-h127.0.0.1", "-P3306" if self.dialect == 
"mysql" else "-P2881", "-u" + user, "-N", "-B"]
+            if self.dialect == "mysql":
+                command += ["-p" + PASSWORD]
+            if database:
+                command += ["shenyu"]
+        elif self.dialect in ("pg", "og"):
+            if self.dialect == "pg":
+                command += ["psql", "-U", "postgres"]
+            else:
+                command += ["/usr/local/opengauss/bin/gsql", "-U", "gaussdb", 
"--password", PASSWORD, "-h", "127.0.0.1"]
+            command += ["-X", "-d", "shenyu", "-v", "ON_ERROR_STOP=1", "-q", 
"-t", "-A"]
+        else:
+            command += ["sqlplus", "-s", "-L", 
f'sqlmatrix/"{PASSWORD}"@//localhost:1521/FREEPDB1']
+            sql = ("whenever sqlerror exit failure rollback\nwhenever oserror 
exit failure\n"
+                   "set define off feedback off heading off pagesize 0 
linesize 32767 trimspool on sqlblanklines on\n"
+                   + sql + "\ncommit;\nexit;\n")
+        output = run(command, sql, timeout)
+        # MySQL's known test-password warning is not a SQL result.
+        output = "\n".join(line for line in output.splitlines() if not 
line.startswith("mysql: [Warning]"))
+        if self.dialect == "oracle" and re.search(r"(?m)^(?:ORA|SP2|TNS)-\d+", 
output):
+            raise RuntimeError("Oracle client/SQL error:\n" + output)
+        return output
+
+    def cleanup(self):
+        logs = subprocess.run(["docker", "logs", self.name], 
capture_output=True, text=True,
+                              encoding="utf-8", errors="replace", timeout=30)
+        (self.output / (self.name + ".log")).write_text(logs.stdout + 
logs.stderr, encoding="utf-8")
+        subprocess.run(["docker", "rm", "-f", self.name], capture_output=True, 
timeout=30, check=False)
+
+    def table(self, name):
+        return '"resource"' if self.dialect == "oracle" and name == "resource" 
else name
+
+    def insert_sentinel(self):
+        binary = {"mysql": f"UNHEX('{JAR_HEX}')", "ob": f"UNHEX('{JAR_HEX}')",
+                  "pg": f"decode('{JAR_HEX}','hex')", "og": 
f"decode('{JAR_HEX}','hex')",
+                  "oracle": f"hextoraw('{JAR_HEX}')"}[self.dialect]
+        self.sql("INSERT INTO plugin 
(id,name,config,role,sort,enabled,plugin_jar) "
+                 f"VALUES 
('{SENTINEL}','sqlMatrixPlugin','{{}}','test',1,1,{binary});")
+
+    def check_data(self, listing, detail):
+        where = f" WHERE id='{SENTINEL}';"
+        for columns in (listing, detail):
+            output = self.sql("SELECT " + ",".join(columns) + " FROM plugin" + 
where)
+            if SENTINEL not in output:
+                raise AssertionError("List/detail projection lost the sentinel 
plugin")
+        binary = {"mysql": "HEX(plugin_jar)", "ob": "HEX(plugin_jar)",
+                  "pg": "encode(plugin_jar,'hex')", "og": 
"encode(plugin_jar,'hex')",
+                  "oracle": 
"rawtohex(dbms_lob.substr(plugin_jar,2000,1))"}[self.dialect]
+        if scalar(self.sql("SELECT " + binary + " FROM plugin" + 
where)).lower() != JAR_HEX:
+            raise AssertionError("Plugin JAR bytes changed")
+        if scalar(self.sql("SELECT name FROM plugin" + where)) != 
"sqlMatrixPlugin":
+            raise AssertionError("Plugin metadata changed")
+
+    def indexes(self):
+        if self.dialect in ("mysql", "ob"):
+            query = "SELECT 
CONCAT(table_name,'|',index_name,'|',column_name,'|',seq_in_index) FROM 
information_schema.statistics WHERE table_schema='shenyu';"
+        elif self.dialect in ("pg", "og"):
+            query = "SELECT tablename||'|'||indexname||'|'||indexdef FROM 
pg_indexes WHERE schemaname='public';"
+        else:
+            query = "SELECT 
lower(table_name)||'|'||lower(index_name)||'|'||lower(column_name)||'|'||column_position
 FROM user_ind_columns;"
+        rows = self.sql(query)
+        (self.output / (self.name + ".indexes.txt")).write_text(rows, 
encoding="utf-8")
+        return verify_index_rows(rows, self.dialect in ("pg", "og"))
+
+
+def execute_matrix(dialect, root, output):
+    output.mkdir(parents=True, exist_ok=True)
+    listing, detail = fragment_columns(root / 
"shenyu-admin/src/main/resources/mappers/plugin-sqlmap.xml")
+    _, schema_path = DIALECTS[dialect]
+    results = {"dialect": dialect, "baseline": BASELINE, "image": 
DIALECTS[dialect][0], "flows": []}
+    for flow in ("upgrade", "fresh"):
+        engine = Engine(dialect, flow, output)
+        start = time.monotonic()
+        report = {"flow": flow, "status": "failure"}
+        results["flows"].append(report)
+        try:
+            engine.start()
+            schema = (run(["git", "show", f"{BASELINE}:{schema_path}"]) if 
flow == "upgrade"
+                      else (root / schema_path).read_text(encoding="utf-8"))
+            engine.sql(schema, database=False)
+            engine.insert_sentinel()
+            original_listing = engine.sql("SELECT " + ",".join(listing) + f" 
FROM plugin WHERE id='{SENTINEL}';")
+            before = {table: set(engine.sql("SELECT id FROM " + 
engine.table(table) + ";").split()) for table in TABLES}
+            if flow == "upgrade":
+                migration = root / 
f"db/upgrade/2.7.1-upgrade-2.7.2-{dialect}.sql"
+                engine.sql(migration.read_text(encoding="utf-8"))
+            engine.check_data(listing, detail)
+            current_listing = engine.sql("SELECT " + ",".join(listing) + f" 
FROM plugin WHERE id='{SENTINEL}';")
+            if current_listing != original_listing:
+                raise AssertionError("Plugin list metadata changed during 
migration")
+            for table, identifiers in before.items():
+                after = set(engine.sql("SELECT id FROM " + engine.table(table) 
+ ";").split())
+                if not identifiers.issubset(after):
+                    raise AssertionError(f"Migration removed rows from 
{table}: {sorted(identifiers - after)}")
+            engine.indexes()
+            report["status"] = "success"
+            print(f"{dialect}: {flow} SQL, projections, JAR preservation and 
index metadata passed", flush=True)
+        except Exception as error:
+            report["error"] = str(error)
+            raise
+        finally:
+            report["seconds"] = round(time.monotonic() - start, 1)
+            (output / "result.json").write_text(json.dumps(results, indent=2), 
encoding="utf-8")
+            engine.cleanup()
+    return results
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument("--dialect", required=True, choices=tuple(DIALECTS))
+    parser.add_argument("--output", required=True, type=Path)
+    args = parser.parse_args()
+    execute_matrix(args.dialect, Path(__file__).resolve().parents[2], 
args.output)
diff --git a/.github/scripts/test-native-sql-matrix.py 
b/.github/scripts/test-native-sql-matrix.py
new file mode 100644
index 0000000000..7685756550
--- /dev/null
+++ b/.github/scripts/test-native-sql-matrix.py
@@ -0,0 +1,230 @@
+# 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.
+
+"""Offline tests for the native SQL harness; these do not execute databases."""
+
+import importlib.util
+import json
+import os
+import re
+import textwrap
+import subprocess
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+from unittest.mock import MagicMock, patch
+
+ROOT = Path(__file__).resolve().parents[2]
+SPEC = importlib.util.spec_from_file_location("matrix", 
Path(__file__).with_name("native-sql-matrix.py"))
+sys.dont_write_bytecode = True
+matrix = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(matrix)
+ROWS = """selector|idx_selector_plugin_id|plugin_id|1
+permission|idx_permission_object_resource|object_id|1
+permission|idx_permission_object_resource|resource_id|2
+permission|idx_permission_resource_id|resource_id|1
+resource|idx_resource_parent|parent_id|1
+user_role|idx_user_role_user_role|user_id|1
+user_role|idx_user_role_user_role|role_id|2
+"""
+
+
+def workflow_python_blocks():
+    workflow = (ROOT / 
".github/workflows/native-sql-matrix.yml").read_text(encoding="utf-8")
+    return [textwrap.dedent(block) for block in re.findall(r"python3 - 
<<'PY'\n(.*?)\n\s+PY", workflow, re.S)]
+
+
+class NativeSqlMatrixTest(unittest.TestCase):
+    def 
test_aggregate_gate_rejects_any_failed_canceled_or_missing_sql_result(self):
+        block = workflow_python_blocks()[-1]
+        base = {"SCOPE_RESULT": "success", "RUNNER_RESULT": "success", 
"REQUIRED": "true",
+                "LIST_RESULT": "success", "SQL_RESULT": "success"}
+        with patch.dict(os.environ, base):
+            exec(block, {})
+        for key in ("SCOPE_RESULT", "RUNNER_RESULT", "LIST_RESULT", 
"SQL_RESULT"):
+            for status in ("failure", "cancelled", "skipped", ""):
+                with patch.dict(os.environ, dict(base, **{key: status})), 
self.assertRaises(AssertionError):
+                    exec(block, {})
+
+    def test_aggregate_not_applicable_requires_explicit_successful_scope(self):
+        block = workflow_python_blocks()[-1]
+        base = {"SCOPE_RESULT": "success", "RUNNER_RESULT": "success", 
"REQUIRED": "false",
+                "LIST_RESULT": "skipped", "SQL_RESULT": "skipped"}
+        with patch.dict(os.environ, base):
+            exec(block, {})
+        for value in ("", "unknown"):
+            with patch.dict(os.environ, dict(base, REQUIRED=value)), 
self.assertRaises(AssertionError):
+                exec(block, {})
+
+    def test_workflow_scope_includes_sql_admin_and_harness_changes(self):
+        block = workflow_python_blocks()[0]
+        for path in ("db/upgrade/test.sql", "db/init/mysql/schema.sql", 
"shenyu-admin/src/test/java/Test.java",
+                     ".github/scripts/native-sql-matrix.py", 
".github/workflows/native-sql-matrix.yml", "README.md"):
+            with tempfile.TemporaryDirectory() as directory:
+                output = Path(directory) / "output"
+                with patch.dict(os.environ, {"BASE_SHA": "1" * 40, 
"EVENT_NAME": "pull_request", "GITHUB_OUTPUT": str(output)}), \
+                        patch.object(subprocess, "run"), 
patch.object(subprocess, "check_output", return_value=path):
+                    exec(block, {})
+                self.assertEqual(output.read_text().strip(), "required=" + 
str(path != "README.md").lower())
+
+    def test_exactly_five_digest_pinned_native_engines(self):
+        self.assertEqual(set(matrix.DIALECTS), {"mysql", "ob", "pg", "og", 
"oracle"})
+        for image, schema in matrix.DIALECTS.values():
+            self.assertRegex(image, r"@sha256:[0-9a-f]{64}$")
+            self.assertTrue((ROOT / schema).is_file())
+        self.assertRegex(matrix.BASELINE, r"^[0-9a-f]{40}$")
+
+    def test_actual_mapper_projects_metadata_and_preserves_detail_jar(self):
+        listing, detail = matrix.fragment_columns(ROOT / 
"shenyu-admin/src/main/resources/mappers/plugin-sqlmap.xml")
+        self.assertNotIn("plugin_jar", listing)
+        self.assertEqual(detail, listing + ["plugin_jar"])
+
+    def test_reject_list_blob_regression(self):
+        xml = '<mapper><sql id="List_Column_List">id,plugin_jar</sql><sql 
id="Base_Column_List">id,plugin_jar</sql></mapper>'
+        with tempfile.TemporaryDirectory() as directory:
+            path = Path(directory) / "mapper.xml"
+            path.write_text(xml, encoding="utf-8")
+            with self.assertRaises(AssertionError):
+                matrix.fragment_columns(path)
+
+    def test_reject_recursive_fragment(self):
+        xml = '<mapper><sql id="List_Column_List"><include 
refid="List_Column_List"/></sql><sql id="Base_Column_List">id</sql></mapper>'
+        with tempfile.TemporaryDirectory() as directory:
+            path = Path(directory) / "mapper.xml"
+            path.write_text(xml, encoding="utf-8")
+            with self.assertRaisesRegex(AssertionError, "Recursive"):
+                matrix.fragment_columns(path)
+
+    def test_mysql_and_oracle_catalogs_check_ordinal_position(self):
+        indexes = matrix.verify_index_rows(ROWS)
+        self.assertEqual(indexes[("permission", 
"idx_permission_object_resource")], ["object_id", "resource_id"])
+
+    def test_composite_nonleading_column_is_not_an_equivalent_index(self):
+        with self.assertRaisesRegex(AssertionError, "permission.resource_id"):
+            
matrix.verify_index_rows(ROWS.replace("permission|idx_permission_resource_id|resource_id|1\n",
 ""))
+
+    def test_wrong_index_column_fails(self):
+        with self.assertRaises(AssertionError):
+            
matrix.verify_index_rows(ROWS.replace("idx_selector_plugin_id|plugin_id", 
"idx_selector_plugin_id|namespace_id"))
+
+    def test_pg_and_og_catalogs(self):
+        rows = "\n".join([
+            "selector|idx_selector_plugin_id|CREATE INDEX 
idx_selector_plugin_id ON public.selector USING btree (plugin_id)",
+            "permission|idx_permission_resource_id|CREATE INDEX 
idx_permission_resource_id ON public.permission USING btree (resource_id)",
+            "permission|permission_object|CREATE INDEX permission_object ON 
public.permission USING btree (object_id, resource_id)",
+            "resource|resource_parent|CREATE INDEX resource_parent ON 
public.resource USING btree (parent_id)",
+            "user_role|user_role_user|CREATE INDEX user_role_user ON 
public.user_role USING btree (user_id, role_id)",
+        ])
+        self.assertEqual(matrix.verify_index_rows(rows, True)[("selector", 
"idx_selector_plugin_id")], ["plugin_id"])
+        opengauss_rows = "\n".join(line + " TABLESPACE pg_default" for line in 
rows.splitlines())
+        self.assertEqual(matrix.verify_index_rows(opengauss_rows, 
True)[("selector", "idx_selector_plugin_id")], ["plugin_id"])
+        partial_rows = rows.replace("(plugin_id)", "(plugin_id) WHERE 
plugin_id IS NOT NULL")
+        with self.assertRaises(AssertionError):
+            matrix.verify_index_rows(partial_rows, True)
+
+    def test_oceanbase_waits_for_writable_ddl_not_only_select(self):
+        with tempfile.TemporaryDirectory() as directory:
+            engine = matrix.Engine("ob", "upgrade", Path(directory))
+            with patch.object(matrix, "run"), patch.object(matrix.time, 
"monotonic", side_effect=[0, 1, 2]), \
+                    patch.object(matrix.time, "sleep") as sleep, \
+                    patch.object(engine, "sql", side_effect=["1", 
RuntimeError("4179 creating tenant"), "1", ""]) as sql:
+                engine.start()
+                self.assertEqual(sql.call_count, 4)
+                self.assertIn("CREATE DATABASE IF NOT EXISTS 
sql_matrix_ready", sql.call_args_list[1].args[0])
+                self.assertIn("DROP DATABASE sql_matrix_ready", 
sql.call_args_list[3].args[0])
+                sleep.assert_called_once_with(5)
+
+    def test_scalars_reject_empty_or_multiple_results(self):
+        self.assertEqual(matrix.scalar("\n 1 \n"), "1")
+        for output in ("", "1\n2", "ERROR\n1"):
+            with self.assertRaises(AssertionError):
+                matrix.scalar(output)
+
+    def test_native_clients_fail_on_sql_error(self):
+        with tempfile.TemporaryDirectory() as directory:
+            for dialect in matrix.DIALECTS:
+                engine = matrix.Engine(dialect, "upgrade", Path(directory))
+                with patch.object(matrix, "run", return_value="1") as invoke:
+                    self.assertEqual(engine.sql("SELECT 1;"), "1")
+                    command, sql, _ = invoke.call_args.args
+                    self.assertNotIn("--force", command)
+                    if dialect in ("pg", "og"):
+                        self.assertIn("ON_ERROR_STOP=1", command)
+                    if dialect == "oracle":
+                        self.assertIn("whenever sqlerror exit failure 
rollback", sql)
+                        self.assertIn("whenever oserror exit failure", sql)
+                        self.assertIn("set define off", sql)
+                        
self.assertIn(f'sqlmatrix/"{matrix.PASSWORD}"@//localhost:1521/FREEPDB1', 
command)
+
+    def test_oracle_client_errors_are_not_false_success(self):
+        with tempfile.TemporaryDirectory() as directory:
+            engine = matrix.Engine("oracle", "upgrade", Path(directory))
+            for output in ("ORA-00942: missing table", "SP2-0042: unknown 
command", "TNS-12541: no listener"):
+                with patch.object(matrix, "run", return_value=output), 
self.assertRaises(RuntimeError):
+                    engine.sql("bad SQL;")
+
+    def test_subprocess_nonzero_sql_exit_is_failure(self):
+        result = subprocess.CompletedProcess([], 1, "syntax error")
+        with patch.object(subprocess, "run", return_value=result), 
self.assertRaisesRegex(RuntimeError, "syntax error"):
+            matrix.run(["fake-client"])
+
+    def test_failed_start_is_recorded_and_cleaned_up(self):
+        with tempfile.TemporaryDirectory() as directory:
+            output = Path(directory)
+            with patch.object(matrix.Engine, "start", 
side_effect=RuntimeError("engine unavailable")), \
+                    patch.object(matrix.Engine, "cleanup") as cleanup:
+                with self.assertRaisesRegex(RuntimeError, "engine 
unavailable"):
+                    matrix.execute_matrix("mysql", ROOT, output)
+                cleanup.assert_called_once()
+                results = json.loads((output / "result.json").read_text())
+                self.assertEqual(results["flows"][0]["status"], "failure")
+                self.assertIn("engine unavailable", 
results["flows"][0]["error"])
+
+    def test_corrupted_jar_is_a_failure(self):
+        with tempfile.TemporaryDirectory() as directory:
+            engine = matrix.Engine("mysql", "upgrade", Path(directory))
+            with patch.object(engine, "sql", side_effect=[matrix.SENTINEL, 
matrix.SENTINEL, "deadbeef"]):
+                with self.assertRaisesRegex(AssertionError, "JAR bytes 
changed"):
+                    engine.check_data(["id"], ["id", "plugin_jar"])
+
+    def 
test_deleted_rows_are_detected_even_if_total_count_does_not_decrease(self):
+        engine = MagicMock()
+        migrated = False
+
+        def sql(query, **kwargs):
+            nonlocal migrated
+            if "idx_selector_plugin_id" in query:
+                migrated = True
+            if query.startswith("SELECT id FROM"):
+                return "new-id" if migrated else "old-id"
+            return "unchanged-list-metadata"
+
+        engine.sql.side_effect = sql
+        with tempfile.TemporaryDirectory() as directory:
+            with patch.object(matrix, "Engine", return_value=engine), 
patch.object(matrix, "run", return_value="-- baseline"):
+                with self.assertRaisesRegex(AssertionError, "removed rows"):
+                    matrix.execute_matrix("mysql", ROOT, Path(directory))
+                engine.cleanup.assert_called_once()
+                result = json.loads((Path(directory) / 
"result.json").read_text())
+                self.assertEqual(result["flows"][0]["status"], "failure")
+
+    def test_oracle_uses_quoted_resource_table(self):
+        self.assertEqual(matrix.Engine("oracle", "upgrade", 
ROOT).table("resource"), '"resource"')
+        self.assertEqual(matrix.Engine("mysql", "upgrade", 
ROOT).table("resource"), "resource")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/.github/workflows/native-sql-matrix.yml 
b/.github/workflows/native-sql-matrix.yml
new file mode 100644
index 0000000000..66c5a41897
--- /dev/null
+++ b/.github/workflows/native-sql-matrix.yml
@@ -0,0 +1,139 @@
+# 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.
+
+name: native-sql-matrix
+
+on:
+  pull_request:
+  push:
+    branches: [master]
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+concurrency:
+  group: native-sql-matrix-${{ github.event.pull_request.number || github.ref 
}}
+  cancel-in-progress: true
+
+jobs:
+  scope:
+    runs-on: ubuntu-latest
+    outputs:
+      required: ${{ steps.scope.outputs.required }}
+    steps:
+      - uses: actions/checkout@v4
+      - name: Determine SQL scope without skipping the aggregate check
+        id: scope
+        env:
+          EVENT_NAME: ${{ github.event_name }}
+          BASE_SHA: ${{ github.event.pull_request.base.sha || 
github.event.before }}
+        run: |
+          python3 - <<'PY'
+          import os, re, subprocess
+          from pathlib import Path
+          base = os.environ.get('BASE_SHA', '')
+          required = True
+          if os.environ['EVENT_NAME'] != 'workflow_dispatch' and 
re.fullmatch(r'[0-9a-f]{40}', base) and set(base) != {'0'}:
+              subprocess.run(['git', 'fetch', '--no-tags', '--depth=1', 
'origin', base], check=True)
+              paths = subprocess.check_output(['git', 'diff', '--name-only', 
base, 'HEAD'], text=True).splitlines()
+              required = any(path.startswith(('db/init/', 'db/upgrade/', 
'shenyu-admin/'))
+                             or path in 
('.github/workflows/native-sql-matrix.yml', 
'.github/scripts/native-sql-matrix.py',
+                                         
'.github/scripts/test-native-sql-matrix.py') for path in paths)
+          with open(os.environ['GITHUB_OUTPUT'], 'a') as output:
+              output.write('required=' + str(required).lower() + '\n')
+          PY
+
+  runner-tests:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - run: python3 .github/scripts/test-native-sql-matrix.py
+
+  list-contract:
+    needs: scope
+    if: needs.scope.outputs.required == 'true'
+    runs-on: ubuntu-latest
+    timeout-minutes: 40
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-java@v4
+        with:
+          distribution: temurin
+          java-version: '17'
+          cache: maven
+      - name: Verify list/detail/export and index contracts
+        run: >-
+          ./mvnw -B -ntp -pl shenyu-admin -am test
+          
-Dtest=PluginMapperTest,PluginServiceTest,PluginControllerTest,AdminQueryIndexTest,PluginIndexUpgradeContractTest
+          -Dsurefire.failIfNoSpecifiedTests=false -Dmaven.javadoc.skip=true
+
+  native-sql:
+    needs: [scope, runner-tests]
+    if: needs.scope.outputs.required == 'true'
+    runs-on: ubuntu-latest
+    timeout-minutes: 40
+    strategy:
+      fail-fast: false
+      matrix:
+        dialect: [mysql, ob, pg, og, oracle]
+    steps:
+      - uses: actions/checkout@v4
+      - name: Fetch immutable 2.7.1 baseline
+        run: git fetch --no-tags --depth=1 origin 
218c5634ebffb1f0ce7e8ea921b85cd5a633cde7
+      - name: Run native fresh-install and upgrade checks
+        env:
+          DIALECT: ${{ matrix.dialect }}
+          RESULTS: ${{ runner.temp }}/sql-matrix/${{ matrix.dialect }}
+        run: python3 .github/scripts/native-sql-matrix.py --dialect "$DIALECT" 
--output "$RESULTS"
+      - name: Upload results and database startup logs
+        if: always()
+        uses: actions/upload-artifact@v4
+        with:
+          name: sql-matrix-${{ matrix.dialect }}
+          path: ${{ runner.temp }}/sql-matrix/${{ matrix.dialect }}
+          if-no-files-found: warn
+      - name: Remove this job's test containers
+        if: always()
+        env:
+          DIALECT: ${{ matrix.dialect }}
+        run: docker ps -aq --filter "name=^/shenyu-sql-${DIALECT}-" | xargs -r 
docker rm -f
+
+  sql-matrix:
+    name: sql-matrix
+    if: always()
+    needs: [scope, runner-tests, list-contract, native-sql]
+    runs-on: ubuntu-latest
+    env:
+      SCOPE_RESULT: ${{ needs.scope.result }}
+      RUNNER_RESULT: ${{ needs.runner-tests.result }}
+      REQUIRED: ${{ needs.scope.outputs.required }}
+      LIST_RESULT: ${{ needs.list-contract.result }}
+      SQL_RESULT: ${{ needs.native-sql.result }}
+    steps:
+      - name: Require every applicable database and list contract
+        run: |
+          python3 - <<'PY'
+          import os
+          assert os.environ['SCOPE_RESULT'] == 'success', 'Scope detection 
failed'
+          assert os.environ['RUNNER_RESULT'] == 'success', 'SQL harness tests 
failed'
+          if os.environ['REQUIRED'] == 'true':
+              assert os.environ['LIST_RESULT'] == 'success', 'List contract 
failed or was canceled/skipped'
+              assert os.environ['SQL_RESULT'] == 'success', 'Native SQL matrix 
failed or was canceled/skipped'
+          else:
+              assert os.environ['REQUIRED'] == 'false', 'Missing scope output'
+              assert os.environ['LIST_RESULT'] == os.environ['SQL_RESULT'] == 
'skipped', 'Unexpected SQL result'
+              print('No SQL/Admin changes; native matrix is not applicable.')
+          PY
diff --git a/db/init/mysql/schema.sql b/db/init/mysql/schema.sql
index e0f6ccc0d2..34485cd43d 100644
--- a/db/init/mysql/schema.sql
+++ b/db/init/mysql/schema.sql
@@ -360,7 +360,8 @@ CREATE TABLE `permission`  (
   `resource_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci 
NOT NULL COMMENT 'resource primary key id',
   `date_created` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 
'create time',
   `date_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE 
CURRENT_TIMESTAMP(3) COMMENT 'update time',
-  PRIMARY KEY (`id`) USING BTREE
+  PRIMARY KEY (`id`) USING BTREE,
+  KEY `idx_permission_resource_id` (`resource_id`) USING BTREE
 ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT 
= 'permission table' ROW_FORMAT = Dynamic;
 
 -- ----------------------------
@@ -2188,7 +2189,8 @@ CREATE TABLE `selector`  (
   `namespace_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci 
NOT NULL COMMENT 'namespace id',
   `date_created` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 
'create time',
   `date_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE 
CURRENT_TIMESTAMP(3) COMMENT 'update time',
-  PRIMARY KEY (`id`) USING BTREE
+  PRIMARY KEY (`id`) USING BTREE,
+  KEY `idx_selector_plugin_id` (`plugin_id`) USING BTREE
 ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci 
ROW_FORMAT = Dynamic;
 
 -- ----------------------------
diff --git a/db/init/ob/schema.sql b/db/init/ob/schema.sql
index 159ff514e4..8eb5d0f2d9 100644
--- a/db/init/ob/schema.sql
+++ b/db/init/ob/schema.sql
@@ -360,7 +360,9 @@ CREATE TABLE `permission`  (
   `resource_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci 
NOT NULL COMMENT 'resource primary key id',
   `date_created` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 
'create time',
   `date_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE 
CURRENT_TIMESTAMP(3) COMMENT 'update time',
-  PRIMARY KEY (`id`) USING BTREE
+  PRIMARY KEY (`id`) USING BTREE,
+  KEY `idx_permission_object_id` (`object_id`) USING BTREE,
+  KEY `idx_permission_resource_id` (`resource_id`) USING BTREE
 ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT 
= 'permission table' ROW_FORMAT = Dynamic;
 
 -- ----------------------------
@@ -1467,7 +1469,8 @@ CREATE TABLE `resource`  (
   `status` int(0) NOT NULL COMMENT 'status 1:enable 0:disable',
   `date_created` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 
'create time',
   `date_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE 
CURRENT_TIMESTAMP(3) COMMENT 'update time',
-  PRIMARY KEY (`id`) USING BTREE
+  PRIMARY KEY (`id`) USING BTREE,
+  KEY `idx_resource_parent_id` (`parent_id`) USING BTREE
 ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT 
= 'resource table' ROW_FORMAT = Dynamic;
 
 -- ----------------------------
@@ -2118,7 +2121,8 @@ CREATE TABLE `selector`  (
   `namespace_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci 
NOT NULL COMMENT 'namespace id',
   `date_created` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 
'create time',
   `date_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE 
CURRENT_TIMESTAMP(3) COMMENT 'update time',
-  PRIMARY KEY (`id`) USING BTREE
+  PRIMARY KEY (`id`) USING BTREE,
+  KEY `idx_selector_plugin_id` (`plugin_id`) USING BTREE
 ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci 
ROW_FORMAT = Dynamic;
 
 -- ----------------------------
@@ -2297,7 +2301,8 @@ CREATE TABLE `user_role`  (
   `role_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT 
NULL COMMENT 'role primary key',
   `date_created` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 
'create time',
   `date_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE 
CURRENT_TIMESTAMP(3) COMMENT 'update time',
-  PRIMARY KEY (`id`) USING BTREE
+  PRIMARY KEY (`id`) USING BTREE,
+  KEY `idx_user_role_user_id` (`user_id`) USING BTREE
 ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT 
= 'user and role bind table' ROW_FORMAT = Dynamic;
 
 -- ----------------------------
diff --git a/db/init/og/create-table.sql b/db/init/og/create-table.sql
index 79d9efa279..55b007b20b 100644
--- a/db/init/og/create-table.sql
+++ b/db/init/og/create-table.sql
@@ -2241,6 +2241,11 @@ COMMENT ON COLUMN "public"."user_role"."date_created" IS 
'create time';
 COMMENT ON COLUMN "public"."user_role"."date_updated" IS 'update time';
 COMMENT ON TABLE "public"."user_role" IS 'user and role bind table';
 
+-- ----------------------------
+-- Index structure for table user_role
+-- ----------------------------
+CREATE INDEX "idx_user_role_user_id" ON "public"."user_role" USING btree 
("user_id");
+
 -- ----------------------------
 -- Records of user_role
 -- ----------------------------
@@ -2317,6 +2322,12 @@ ALTER TABLE "public"."operation_record_log" ADD 
CONSTRAINT "operation_record_log
 -- ----------------------------
 ALTER TABLE "public"."permission" ADD CONSTRAINT "permission_pkey" PRIMARY KEY 
("id");
 
+-- ----------------------------
+-- Index structure for table permission
+-- ----------------------------
+CREATE INDEX "idx_permission_object_id" ON "public"."permission" USING btree 
("object_id");
+CREATE INDEX "idx_permission_resource_id" ON "public"."permission" USING btree 
("resource_id");
+
 -- ----------------------------
 -- Primary Key structure for table plugin
 -- ----------------------------
@@ -2348,6 +2359,11 @@ ALTER TABLE "public"."plugin_handle" ADD CONSTRAINT 
"plugin_handle_pkey" PRIMARY
 -- ----------------------------
 ALTER TABLE "public"."resource" ADD CONSTRAINT "resource_pkey" PRIMARY KEY 
("id");
 
+-- ----------------------------
+-- Index structure for table resource
+-- ----------------------------
+CREATE INDEX "idx_resource_parent_id" ON "public"."resource" USING btree 
("parent_id");
+
 -- ----------------------------
 -- Primary Key structure for table role
 -- ----------------------------
@@ -2368,6 +2384,11 @@ ALTER TABLE "public"."rule_condition" ADD CONSTRAINT 
"rule_condition_pkey" PRIMA
 -- ----------------------------
 ALTER TABLE "public"."selector" ADD CONSTRAINT "selector_pkey" PRIMARY KEY 
("id");
 
+-- ----------------------------
+-- Index structure for table selector
+-- ----------------------------
+CREATE INDEX "idx_selector_plugin_id" ON "public"."selector" USING btree 
("plugin_id");
+
 -- ----------------------------
 -- Rules structure for table shenyu_dict
 -- ----------------------------
diff --git a/db/init/oracle/schema.sql b/db/init/oracle/schema.sql
index 89904f4da8..4f03708f2a 100644
--- a/db/init/oracle/schema.sql
+++ b/db/init/oracle/schema.sql
@@ -129,6 +129,8 @@ create table selector
     date_created timestamp(3) default SYSDATE not null,
     date_updated timestamp(3) default SYSDATE not null
 );
+
+create index idx_selector_plugin_id on selector (plugin_id);
 -- Add comments to the columns
 comment on column SELECTOR.id
   is 'primary key id varchar';
@@ -667,6 +669,8 @@ create table user_role
     date_updated timestamp(3) default SYSDATE not null,
     PRIMARY KEY (id)
 );
+
+create index idx_user_role_user_id on user_role (user_id);
 -- Add comments to the table
 comment on table USER_ROLE
   is 'user and role bind table';
@@ -730,6 +734,9 @@ create table permission
     date_updated timestamp(3) default SYSDATE not null,
     PRIMARY KEY (id)
 );
+
+create index idx_permission_object_id on permission (object_id);
+create index idx_permission_resource_id on permission (resource_id);
 -- Add comments to the table
 comment on table PERMISSION
   is 'permission table';
@@ -764,6 +771,8 @@ create table "resource"
     date_updated  timestamp(3) default SYSDATE not null,
     PRIMARY KEY (id)
 );
+
+create index idx_resource_parent_id on "resource" (parent_id);
 -- Add comments to the table
 comment on table "resource"
   is 'resource table';
diff --git a/db/init/pg/create-table.sql b/db/init/pg/create-table.sql
index ada3867541..f18208283f 100644
--- a/db/init/pg/create-table.sql
+++ b/db/init/pg/create-table.sql
@@ -2364,6 +2364,11 @@ COMMENT ON COLUMN "public"."user_role"."date_created" IS 
'create time';
 COMMENT ON COLUMN "public"."user_role"."date_updated" IS 'update time';
 COMMENT ON TABLE "public"."user_role" IS 'user and role bind table';
 
+-- ----------------------------
+-- Index structure for table user_role
+-- ----------------------------
+CREATE INDEX "idx_user_role_user_id" ON "public"."user_role" USING btree 
("user_id");
+
 -- ----------------------------
 -- Records of user_role
 -- ----------------------------
@@ -2440,6 +2445,12 @@ ALTER TABLE "public"."operation_record_log" ADD 
CONSTRAINT "operation_record_log
 -- ----------------------------
 ALTER TABLE "public"."permission" ADD CONSTRAINT "permission_pkey" PRIMARY KEY 
("id");
 
+-- ----------------------------
+-- Index structure for table permission
+-- ----------------------------
+CREATE INDEX "idx_permission_object_id" ON "public"."permission" USING btree 
("object_id");
+CREATE INDEX "idx_permission_resource_id" ON "public"."permission" USING btree 
("resource_id");
+
 -- ----------------------------
 -- Primary Key structure for table plugin
 -- ----------------------------
@@ -2471,6 +2482,11 @@ ALTER TABLE "public"."plugin_handle" ADD CONSTRAINT 
"plugin_handle_pkey" PRIMARY
 -- ----------------------------
 ALTER TABLE "public"."resource" ADD CONSTRAINT "resource_pkey" PRIMARY KEY 
("id");
 
+-- ----------------------------
+-- Index structure for table resource
+-- ----------------------------
+CREATE INDEX "idx_resource_parent_id" ON "public"."resource" USING btree 
("parent_id");
+
 -- ----------------------------
 -- Primary Key structure for table role
 -- ----------------------------
@@ -2491,6 +2507,11 @@ ALTER TABLE "public"."rule_condition" ADD CONSTRAINT 
"rule_condition_pkey" PRIMA
 -- ----------------------------
 ALTER TABLE "public"."selector" ADD CONSTRAINT "selector_pkey" PRIMARY KEY 
("id");
 
+-- ----------------------------
+-- Index structure for table selector
+-- ----------------------------
+CREATE INDEX "idx_selector_plugin_id" ON "public"."selector" USING btree 
("plugin_id");
+
 -- ----------------------------
 -- Rules structure for table shenyu_dict
 -- ----------------------------
diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql 
b/db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql
index f049f0b593..04104c6207 100644
--- a/db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql
+++ b/db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql
@@ -54,6 +54,10 @@ INSERT INTO `permission` (`id`, `object_id`, `resource_id`, 
`date_created`, `dat
 INSERT INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, 
`date_updated`) VALUES ('1953049887387303974', '1346358560427216896', 
'1953048313980116914', '2026-09-21 00:00:00', '2026-09-21 00:00:00');
 INSERT INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, 
`sort`, `enabled`, `date_created`, `date_updated`) VALUES 
('1907261515594055681', '649330b6-c2d7-4edc-be8e-8a54df9eb385', '67', null, 
197, 0, '2026-09-21 00:00:00', '2026-09-21 00:00:00');
 
+-- add indexes to speed up the admin plugin snapshot and permission queries
+ALTER TABLE `selector` ADD INDEX `idx_selector_plugin_id` (`plugin_id`) USING 
BTREE;
+ALTER TABLE `permission` ADD INDEX `idx_permission_resource_id` 
(`resource_id`) USING BTREE;
+
 -- Agent Gateway: keep upgrade seeds consistent with the fresh-install schema.
 INSERT INTO `plugin` VALUES ('68', 'agentGateway', null, 'Ai', 198, 0, 
'2026-09-19 00:00:00', '2026-09-19 00:00:00', null);
 INSERT INTO `plugin_handle` VALUES ('1960000000000001000', '68', 
'trafficType', 'trafficType', 2, 2, 0, 
'{"required":"1","defaultValue":"LLM","rule":""}', '2026-09-19 00:00:00', 
'2026-09-19 00:00:00');
diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-ob.sql 
b/db/upgrade/2.7.1-upgrade-2.7.2-ob.sql
index 2a807e7739..162849ebdc 100644
--- a/db/upgrade/2.7.1-upgrade-2.7.2-ob.sql
+++ b/db/upgrade/2.7.1-upgrade-2.7.2-ob.sql
@@ -54,6 +54,13 @@ INSERT INTO `permission` (`id`, `object_id`, `resource_id`, 
`date_created`, `dat
 INSERT INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, 
`date_updated`) VALUES ('1953049887387303974', '1346358560427216896', 
'1953048313980116914', '2026-09-21 00:00:00', '2026-09-21 00:00:00');
 INSERT INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, 
`sort`, `enabled`, `date_created`, `date_updated`) VALUES 
('1907261515594055681', '649330b6-c2d7-4edc-be8e-8a54df9eb385', '67', NULL, 
197, 0, '2026-09-21 00:00:00', '2026-09-21 00:00:00');
 
+-- add indexes to speed up the admin plugin snapshot and permission queries
+ALTER TABLE `selector` ADD INDEX `idx_selector_plugin_id` (`plugin_id`) USING 
BTREE;
+ALTER TABLE `permission` ADD INDEX `idx_permission_object_id` (`object_id`) 
USING BTREE;
+ALTER TABLE `permission` ADD INDEX `idx_permission_resource_id` 
(`resource_id`) USING BTREE;
+ALTER TABLE `resource` ADD INDEX `idx_resource_parent_id` (`parent_id`) USING 
BTREE;
+ALTER TABLE `user_role` ADD INDEX `idx_user_role_user_id` (`user_id`) USING 
BTREE;
+
 -- Agent Gateway: keep upgrade seeds consistent with the fresh-install schema.
 INSERT INTO `plugin` VALUES ('68', 'agentGateway', NULL, 'Ai', 198, 0, 
'2026-09-19 00:00:00', '2026-09-19 00:00:00', null);
 INSERT INTO `plugin_handle` VALUES ('1960000000000001000', '68', 
'trafficType', 'trafficType', 2, 2, 0, 
'{"required":"1","defaultValue":"LLM","rule":""}', '2026-09-19 00:00:00', 
'2026-09-19 00:00:00');
diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-og.sql 
b/db/upgrade/2.7.1-upgrade-2.7.2-og.sql
index 52ca18a73c..b6524e0fc9 100644
--- a/db/upgrade/2.7.1-upgrade-2.7.2-og.sql
+++ b/db/upgrade/2.7.1-upgrade-2.7.2-og.sql
@@ -54,6 +54,13 @@ INSERT INTO "public"."permission" VALUES 
('1953049887387303973', '13463585604272
 INSERT INTO "public"."permission" VALUES ('1953049887387303974', 
'1346358560427216896', '1953048313980116914', '2026-09-21 00:00:00.000', 
'2026-09-21 00:00:00.000');
 INSERT INTO "public"."namespace_plugin_rel" VALUES ('1907261515594055681', 
'649330b6-c2d7-4edc-be8e-8a54df9eb385', '67', NULL, 197, 0, '2026-09-21 
00:00:00.000', '2026-09-21 00:00:00.000');
 
+-- add indexes to speed up the admin plugin snapshot and permission queries
+CREATE INDEX IF NOT EXISTS "idx_selector_plugin_id" ON "public"."selector" 
USING btree ("plugin_id");
+CREATE INDEX IF NOT EXISTS "idx_permission_object_id" ON "public"."permission" 
USING btree ("object_id");
+CREATE INDEX IF NOT EXISTS "idx_permission_resource_id" ON 
"public"."permission" USING btree ("resource_id");
+CREATE INDEX IF NOT EXISTS "idx_resource_parent_id" ON "public"."resource" 
USING btree ("parent_id");
+CREATE INDEX IF NOT EXISTS "idx_user_role_user_id" ON "public"."user_role" 
USING btree ("user_id");
+
 -- Agent Gateway: keep upgrade seeds consistent with the fresh-install schema.
 INSERT INTO "public"."plugin" VALUES ('68', 'agentGateway', NULL, 'Ai', 198, 
0, '2026-09-19 00:00:00', '2026-09-19 00:00:00', NULL);
 INSERT INTO "public"."plugin_handle" VALUES ('1960000000000001000', '68', 
'trafficType', 'trafficType', 2, 2, 0, 
'{"required":"1","defaultValue":"LLM","rule":""}', '2026-09-19 00:00:00', 
'2026-09-19 00:00:00');
diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql 
b/db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql
index e7d6b2f446..3705cc8af3 100644
--- a/db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql
+++ b/db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql
@@ -54,6 +54,13 @@ INSERT INTO permission (id, object_id, resource_id, 
date_created, date_updated)
 INSERT INTO permission (id, object_id, resource_id, date_created, 
date_updated) VALUES ('1953049887387303974', '1346358560427216896', 
'1953048313980116914', sysdate, sysdate);
 INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(namespace_plugin_rel(id)) */ INTO 
namespace_plugin_rel (id, namespace_id, plugin_id, config, sort, enabled, 
date_created, date_updated) VALUES ('1907261515594055681', 
'649330b6-c2d7-4edc-be8e-8a54df9eb385', '67', NULL, 197, 0, sysdate, sysdate);
 
+-- add indexes to speed up the admin plugin snapshot and permission queries
+create index idx_selector_plugin_id on selector (plugin_id);
+create index idx_permission_object_id on permission (object_id);
+create index idx_permission_resource_id on permission (resource_id);
+create index idx_resource_parent_id on "resource" (parent_id);
+create index idx_user_role_user_id on user_role (user_id);
+
 -- Agent Gateway: keep upgrade seeds consistent with the fresh-install schema.
 INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(plugin(id)) */ INTO plugin (id, name, 
config, role, sort, enabled, date_created, date_updated)
 VALUES ('68', 'agentGateway', NULL, 'Ai', 198, 0, sysdate, sysdate);
diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-pg.sql 
b/db/upgrade/2.7.1-upgrade-2.7.2-pg.sql
index fcfe36f1b3..87bd1e1dee 100644
--- a/db/upgrade/2.7.1-upgrade-2.7.2-pg.sql
+++ b/db/upgrade/2.7.1-upgrade-2.7.2-pg.sql
@@ -54,6 +54,13 @@ INSERT INTO "public"."permission" VALUES 
('1953049887387303973', '13463585604272
 INSERT INTO "public"."permission" VALUES ('1953049887387303974', 
'1346358560427216896', '1953048313980116914', '2026-09-21 00:00:00.000', 
'2026-09-21 00:00:00.000');
 INSERT INTO "public"."namespace_plugin_rel" VALUES ('1907261515594055681', 
'649330b6-c2d7-4edc-be8e-8a54df9eb385', '67', NULL, 197, 0, '2026-09-21 
00:00:00.000', '2026-09-21 00:00:00.000');
 
+-- add indexes to speed up the admin plugin snapshot and permission queries
+CREATE INDEX IF NOT EXISTS "idx_selector_plugin_id" ON "public"."selector" 
USING btree ("plugin_id");
+CREATE INDEX IF NOT EXISTS "idx_permission_object_id" ON "public"."permission" 
USING btree ("object_id");
+CREATE INDEX IF NOT EXISTS "idx_permission_resource_id" ON 
"public"."permission" USING btree ("resource_id");
+CREATE INDEX IF NOT EXISTS "idx_resource_parent_id" ON "public"."resource" 
USING btree ("parent_id");
+CREATE INDEX IF NOT EXISTS "idx_user_role_user_id" ON "public"."user_role" 
USING btree ("user_id");
+
 -- Agent Gateway: keep upgrade seeds consistent with the fresh-install schema.
 INSERT INTO "public"."plugin" VALUES ('68', 'agentGateway', NULL, 'Ai', 198, 
0, '2026-09-19 00:00:00', '2026-09-19 00:00:00', NULL);
 INSERT INTO "public"."plugin_handle" VALUES ('1960000000000001000', '68', 
'trafficType', 'trafficType', 2, 2, 0, 
'{"required":"1","defaultValue":"LLM","rule":""}', '2026-09-19 00:00:00', 
'2026-09-19 00:00:00');
diff --git a/db/upgrade/native-sql-matrix.md b/db/upgrade/native-sql-matrix.md
new file mode 100644
index 0000000000..618ef73b31
--- /dev/null
+++ b/db/upgrade/native-sql-matrix.md
@@ -0,0 +1,70 @@
+# Native SQL compatibility merge gate
+
+The `native-sql-matrix` workflow reports a stable aggregate check named
+`sql-matrix`. Maintainers can configure this check as required after validating
+the workflow on their infrastructure. Adding the workflow does not change
+repository branch-protection or ruleset settings.
+
+For SQL/schema, Admin, or matrix-harness changes the gate requires:
+
+- the offline harness regression tests;
+- the real H2 mapper/service/controller list, detail/export and index tests;
+- all five native database jobs (MySQL, OceanBase MySQL mode, PostgreSQL,
+  openGauss, Oracle Free), each running both upgrade and fresh-install flows.
+
+Failures, cancellations, skipped applicable jobs, missing scope output and
+startup timeouts fail the gate. An unrelated change explicitly reports the
+matrix as not applicable; the aggregate still runs. The matrix uses
+`fail-fast: false` so one engine does not cancel the other engines.
+
+## Native execution
+
+Database images are digest-pinned in `.github/scripts/native-sql-matrix.py`:
+MySQL 8.0, PostgreSQL 15, openGauss lite 5.0.1, OceanBase 4.3.5 LTS and Oracle
+Free 23.26.3 slim. This validates these selected engine versions, not every
+vendor/version combination. Images run in disposable Linux amd64 containers,
+without published ports, production credentials, host mounts or privileged
+mode. Each dialect gets an independent runner and a 40-minute job timeout.
+
+The upgrade starts from the actual `v2.7.1` schema at immutable commit
+`218c5634ebffb1f0ce7e8ea921b85cd5a633cde7` and executes the complete current
+`2.7.1-upgrade-2.7.2-<dialect>.sql` without rewriting or filtering its SQL.
+The fresh flow executes the complete current initialization schema in a new
+container, not the already upgraded database. Clients abort on SQL errors;
+there is no `--force`, tolerated-error list or fallback to H2.
+
+Both flows insert a plugin with a nonempty binary JAR including zero/high-bit
+bytes. The checks execute the actual shared mapper column projections, require
+the list projection to exclude `plugin_jar`, preserve all sentinel list 
metadata
+and JAR bytes, preserve pre-upgrade row IDs in plugin/selector/rule/resource/
+permission/user-role/namespace-plugin relation tables, and inspect native index
+catalogs. Composite indexes only count when the requested column leads the 
index.
+The exact new selector/plugin and permission/resource indexes must also exist.
+
+This is not a full migration idempotency test: the existing upgrade scripts are
+one-time migrations. PostgreSQL/openGauss `IF NOT EXISTS` clauses are 
additionally
+guarded by `PluginIndexUpgradeContractTest`. Native queries exercise mapper 
column
+projections; actual Java mapper/service/API contracts are tested separately on 
H2.
+Retained row IDs and sentinel fields are checked, not a full content 
fingerprint
+of every production table. No gateway request or Docker e2e coverage is 
claimed.
+
+Per-engine artifacts contain flow results, durations and startup logs, 
including
+failed attempts. Cleanup is restricted to that job's generated test containers.
+
+## Local execution
+
+With a Linux Docker daemon available, from the repository root:
+
+```sh
+git fetch --no-tags --depth=1 origin 218c5634ebffb1f0ce7e8ea921b85cd5a633cde7
+python3 -B .github/scripts/test-native-sql-matrix.py
+python3 .github/scripts/native-sql-matrix.py --dialect mysql --output 
/tmp/shenyu-sql-matrix/mysql
+```
+
+Use `ob`, `pg`, `og` and `oracle` for the other native jobs. The workflow is 
also
+manually dispatchable to force all jobs even without SQL changes. Offline 
Python
+tests and workflow lint passing alone do **not** mean the native matrix passed;
+require `sql-matrix` success on the current PR head before claiming 
verification.
+
+Image setup follows the [OceanBase container 
guide](https://hub.docker.com/r/oceanbase/oceanbase-ce)
+and [Oracle Free image 
documentation](https://github.com/gvenzl/oci-oracle-free).
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/PluginController.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/PluginController.java
index ab6e5e4787..459aab93c8 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/PluginController.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/PluginController.java
@@ -27,6 +27,7 @@ import org.apache.shenyu.admin.model.page.PageParameter;
 import org.apache.shenyu.admin.model.query.PluginQuery;
 import org.apache.shenyu.admin.model.query.PluginQueryCondition;
 import org.apache.shenyu.admin.model.result.ShenyuAdminResult;
+import org.apache.shenyu.admin.model.vo.PluginListVO;
 import org.apache.shenyu.admin.model.vo.PluginVO;
 import org.apache.shenyu.admin.service.PageService;
 import org.apache.shenyu.admin.service.PluginService;
@@ -52,7 +53,7 @@ import java.util.List;
  * this is plugin controller.
  */
 @RestApi("/plugin-template")
-public class PluginController implements PagedController<PluginQueryCondition, 
PluginVO> {
+public class PluginController implements PagedController<PluginQueryCondition, 
PluginListVO> {
 
     private final PluginService pluginService;
     
@@ -74,7 +75,7 @@ public class PluginController implements 
PagedController<PluginQueryCondition, P
     public ShenyuAdminResult queryPlugins(final String name, final Integer 
enabled, final String role,
                                           @NotNull final Integer currentPage,
                                           @NotNull final Integer pageSize) {
-        CommonPager<PluginVO> commonPager = pluginService.listByPage(new 
PluginQuery(name, enabled, role, new PageParameter(currentPage, pageSize)));
+        CommonPager<PluginListVO> commonPager = pluginService.listByPage(new 
PluginQuery(name, enabled, role, new PageParameter(currentPage, pageSize)));
         return ShenyuAdminResult.success(ShenyuResultMessage.QUERY_SUCCESS, 
commonPager);
     }
     
@@ -194,7 +195,7 @@ public class PluginController implements 
PagedController<PluginQueryCondition, P
     }
     
     @Override
-    public PageService<PluginQueryCondition, PluginVO> pageService() {
+    public PageService<PluginQueryCondition, PluginListVO> pageService() {
         return pluginService;
     }
 }
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/PluginMapper.java 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/PluginMapper.java
index f4a2747a6c..2783f12a17 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/PluginMapper.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/PluginMapper.java
@@ -23,7 +23,7 @@ import org.apache.shenyu.admin.model.entity.PluginDO;
 import org.apache.shenyu.admin.model.query.PluginQuery;
 import org.apache.shenyu.admin.model.query.PluginQueryCondition;
 import org.apache.shenyu.admin.model.vo.PluginSnapshotVO;
-import org.apache.shenyu.admin.model.vo.PluginVO;
+import org.apache.shenyu.admin.model.vo.PluginListVO;
 import org.apache.shenyu.admin.validation.ExistProvider;
 
 import java.io.Serializable;
@@ -211,5 +211,5 @@ public interface PluginMapper extends ExistProvider {
      * @param condition condition.
      * @return list
      */
-    List<PluginVO> searchByCondition(@Param("condition") PluginQueryCondition 
condition);
+    List<PluginListVO> searchByCondition(@Param("condition") 
PluginQueryCondition condition);
 }
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/vo/PluginListVO.java 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/vo/PluginListVO.java
new file mode 100644
index 0000000000..63387c0e50
--- /dev/null
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/vo/PluginListVO.java
@@ -0,0 +1,277 @@
+/*
+ * 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.
+ */
+
+package org.apache.shenyu.admin.model.vo;
+
+import com.google.common.collect.Lists;
+import org.apache.shenyu.admin.model.entity.PluginDO;
+import org.apache.shenyu.common.utils.DateUtils;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * Plugin list response containing metadata only, without JAR content.
+ */
+public class PluginListVO implements Serializable {
+
+    private static final long serialVersionUID = 7537793180460522887L;
+
+    /**
+     * primary key.
+     */
+    private String id;
+
+    /**
+     * plugin role.
+     */
+    private String role;
+
+    /**
+     * plugin name.
+     */
+    private String name;
+
+    /**
+     * plugin config.
+     */
+    private String config;
+
+    /**
+     * plugin sort.
+     */
+    private Integer sort;
+
+    /**
+     * whether enabled.
+     */
+    private Boolean enabled;
+
+    /**
+     * created time.
+     */
+    private String dateCreated;
+
+    /**
+     * updated time.
+     */
+    private String dateUpdated;
+
+    /**
+     * plugin handle List.
+     */
+    private List<PluginHandleVO> pluginHandleList;
+
+    public PluginListVO() {
+    }
+
+    public PluginListVO(final String id,
+                    final String role,
+                    final String name,
+                    final String config,
+                    final Integer sort,
+                    final Boolean enabled,
+                    final String dateCreated,
+                    final String dateUpdated,
+                    final List<PluginHandleVO> pluginHandleList) {
+        this.id = id;
+        this.role = role;
+        this.name = name;
+        this.config = config;
+        this.sort = sort;
+        this.enabled = enabled;
+        this.dateCreated = dateCreated;
+        this.dateUpdated = dateUpdated;
+        this.pluginHandleList = pluginHandleList;
+    }
+
+    /**
+     * Gets the value of id.
+     *
+     * @return the value of id
+     */
+    public String getId() {
+        return id;
+    }
+
+    /**
+     * Sets the id.
+     *
+     * @param id id
+     */
+    public void setId(final String id) {
+        this.id = id;
+    }
+
+    /**
+     * Gets the value of role.
+     *
+     * @return the value of role
+     */
+    public String getRole() {
+        return role;
+    }
+
+    /**
+     * Sets the role.
+     *
+     * @param role role
+     */
+    public void setRole(final String role) {
+        this.role = role;
+    }
+
+    /**
+     * Gets the value of name.
+     *
+     * @return the value of name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Sets the name.
+     *
+     * @param name name
+     */
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+    /**
+     * Gets the value of config.
+     *
+     * @return the value of config
+     */
+    public String getConfig() {
+        return config;
+    }
+
+    /**
+     * Sets the config.
+     *
+     * @param config config
+     */
+    public void setConfig(final String config) {
+        this.config = config;
+    }
+
+    /**
+     * Gets the value of sort.
+     *
+     * @return the value of sort
+     */
+    public Integer getSort() {
+        return sort;
+    }
+
+    /**
+     * Sets the sort.
+     *
+     * @param sort sort
+     */
+    public void setSort(final Integer sort) {
+        this.sort = sort;
+    }
+
+    /**
+     * Gets the value of enabled.
+     *
+     * @return the value of enabled
+     */
+    public Boolean getEnabled() {
+        return enabled;
+    }
+
+    /**
+     * Sets the enabled.
+     *
+     * @param enabled enabled
+     */
+    public void setEnabled(final Boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    /**
+     * Gets the value of dateCreated.
+     *
+     * @return the value of dateCreated
+     */
+    public String getDateCreated() {
+        return dateCreated;
+    }
+
+    /**
+     * Sets the dateCreated.
+     *
+     * @param dateCreated dateCreated
+     */
+    public void setDateCreated(final String dateCreated) {
+        this.dateCreated = dateCreated;
+    }
+
+    /**
+     * Gets the value of dateUpdated.
+     *
+     * @return the value of dateUpdated
+     */
+    public String getDateUpdated() {
+        return dateUpdated;
+    }
+
+    /**
+     * Sets the dateUpdated.
+     *
+     * @param dateUpdated dateUpdated
+     */
+    public void setDateUpdated(final String dateUpdated) {
+        this.dateUpdated = dateUpdated;
+    }
+
+    /**
+     * Gets the plugin handle list.
+     *
+     * @return the plugin handle list
+     */
+    public List<PluginHandleVO> getPluginHandleList() {
+        return pluginHandleList;
+    }
+
+    /**
+     * Sets the the plugin handle list.
+     *
+     * @param pluginHandleList the plugin handle list
+     */
+    public void setPluginHandleList(final List<PluginHandleVO> 
pluginHandleList) {
+        this.pluginHandleList = pluginHandleList;
+    }
+
+    /**
+     * build pluginVO.
+     *
+     * @param pluginDO {@linkplain PluginDO}
+     * @return {@linkplain PluginListVO}
+     */
+    public static PluginListVO buildPluginListVO(final PluginDO pluginDO) {
+        return new PluginListVO(pluginDO.getId(), pluginDO.getRole(), 
pluginDO.getName(),
+                pluginDO.getConfig(), pluginDO.getSort(), 
pluginDO.getEnabled(),
+                
DateUtils.localDateTimeToString(pluginDO.getDateCreated().toLocalDateTime()),
+                
DateUtils.localDateTimeToString(pluginDO.getDateUpdated().toLocalDateTime()),
+                Lists.newArrayList());
+    }
+}
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/PluginService.java 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/PluginService.java
index 13105e1e70..e28d428d34 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/PluginService.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/PluginService.java
@@ -24,6 +24,7 @@ import org.apache.shenyu.admin.model.query.PluginQuery;
 import org.apache.shenyu.admin.model.query.PluginQueryCondition;
 import org.apache.shenyu.admin.model.result.ConfigImportResult;
 import org.apache.shenyu.admin.model.vo.PluginSnapshotVO;
+import org.apache.shenyu.admin.model.vo.PluginListVO;
 import org.apache.shenyu.admin.model.vo.PluginVO;
 import org.apache.shenyu.admin.service.configs.ConfigsImportContext;
 import org.apache.shenyu.common.dto.PluginData;
@@ -33,7 +34,7 @@ import java.util.List;
 /**
  * this is plugin service.
  */
-public interface PluginService extends PageService<PluginQueryCondition, 
PluginVO> {
+public interface PluginService extends PageService<PluginQueryCondition, 
PluginListVO> {
 
     /**
      * Create or update string.
@@ -72,7 +73,7 @@ public interface PluginService extends 
PageService<PluginQueryCondition, PluginV
      * @param pluginQuery {@linkplain PluginQuery}
      * @return {@linkplain CommonPager}
      */
-    CommonPager<PluginVO> listByPage(PluginQuery pluginQuery);
+    CommonPager<PluginListVO> listByPage(PluginQuery pluginQuery);
     
     /**
      * List all list.
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/PluginServiceImpl.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/PluginServiceImpl.java
index 2bcea4503b..3b23efbad8 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/PluginServiceImpl.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/PluginServiceImpl.java
@@ -31,6 +31,7 @@ import org.apache.shenyu.admin.model.query.PluginQuery;
 import org.apache.shenyu.admin.model.query.PluginQueryCondition;
 import org.apache.shenyu.admin.model.result.ConfigImportResult;
 import org.apache.shenyu.admin.model.vo.PluginSnapshotVO;
+import org.apache.shenyu.admin.model.vo.PluginListVO;
 import org.apache.shenyu.admin.model.vo.PluginVO;
 import org.apache.shenyu.admin.service.PluginService;
 import org.apache.shenyu.admin.service.configs.ConfigsImportContext;
@@ -87,7 +88,7 @@ public class PluginServiceImpl implements PluginService {
     }
 
     @Override
-    public List<PluginVO> searchByCondition(final PluginQueryCondition 
condition) {
+    public List<PluginListVO> searchByCondition(final PluginQueryCondition 
condition) {
         condition.init();
         return pluginMapper.searchByCondition(condition);
     }
@@ -184,10 +185,10 @@ public class PluginServiceImpl implements PluginService {
      */
     @Override
     @Pageable
-    public CommonPager<PluginVO> listByPage(final PluginQuery pluginQuery) {
+    public CommonPager<PluginListVO> listByPage(final PluginQuery pluginQuery) 
{
         return PageResultUtils.result(pluginQuery.getPageParameter(), () -> 
pluginMapper.selectByQuery(pluginQuery)
                 .stream()
-                .map(PluginVO::buildPluginVO)
+                .map(PluginListVO::buildPluginListVO)
                 .collect(Collectors.toList()));
     }
 
diff --git a/shenyu-admin/src/main/resources/mappers/plugin-sqlmap.xml 
b/shenyu-admin/src/main/resources/mappers/plugin-sqlmap.xml
index da687b1c7c..192a7eb84b 100644
--- a/shenyu-admin/src/main/resources/mappers/plugin-sqlmap.xml
+++ b/shenyu-admin/src/main/resources/mappers/plugin-sqlmap.xml
@@ -31,6 +31,11 @@
     </resultMap>
 
     <sql id="Base_Column_List">
+        <include refid="List_Column_List"/>,
+        plugin_jar
+    </sql>
+
+    <sql id="List_Column_List">
         id,
         date_created,
         date_updated,
@@ -38,7 +43,6 @@
         config,
         role,
         sort,
-        plugin_jar,
         enabled
     </sql>
 
@@ -86,7 +90,7 @@
     <select id="selectByQuery" 
parameterType="org.apache.shenyu.admin.model.query.PluginQuery"
             resultMap="BaseResultMap">
         SELECT
-               <include refid="Base_Column_List"/>
+               <include refid="List_Column_List"/>
           FROM plugin
         <where>
             <if test="name != null and name != ''">
@@ -211,10 +215,10 @@
         order by p.sort, p.id
     </select>
 
-    <select id="searchByCondition" 
resultType="org.apache.shenyu.admin.model.vo.PluginVO">
+    <select id="searchByCondition" 
resultType="org.apache.shenyu.admin.model.vo.PluginListVO">
 
         SELECT
-        <include refid="Base_Column_List"/>
+        <include refid="List_Column_List"/>
         FROM plugin
         <where>
             <if test="condition.keyword != null and condition.keyword != ''">
diff --git a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql 
b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
index 4fcb873aea..2f7f6e6d12 100644
--- a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
+++ b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
@@ -1754,3 +1754,7 @@ CREATE INDEX idx_namespace_user_ns_user ON 
`namespace_user_rel` (namespace_id, u
 CREATE INDEX idx_namespace_user_user_ns ON `namespace_user_rel` (user_id, 
namespace_id);
 CREATE INDEX idx_user_role_user_role ON `user_role` (user_id, role_id);
 CREATE INDEX idx_resource_parent ON `resource` (parent_id);
+
+-- Plugin snapshot and permission query indexes.
+CREATE INDEX IF NOT EXISTS `idx_selector_plugin_id` ON `selector` 
(`plugin_id`);
+CREATE INDEX IF NOT EXISTS `idx_permission_resource_id` ON `permission` 
(`resource_id`);
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/PluginControllerTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/PluginControllerTest.java
index 897ac861f9..3372517480 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/PluginControllerTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/PluginControllerTest.java
@@ -26,6 +26,7 @@ import org.apache.shenyu.admin.model.dto.PluginDTO;
 import org.apache.shenyu.admin.model.page.CommonPager;
 import org.apache.shenyu.admin.model.page.PageParameter;
 import org.apache.shenyu.admin.model.query.PluginQuery;
+import org.apache.shenyu.admin.model.vo.PluginListVO;
 import org.apache.shenyu.admin.model.vo.PluginVO;
 import org.apache.shenyu.admin.service.PluginService;
 import org.apache.shenyu.admin.service.SyncDataService;
@@ -100,13 +101,15 @@ public final class PluginControllerTest {
     @Test
     public void testQueryPlugins() throws Exception {
         final PageParameter pageParameter = new PageParameter();
-        List<PluginVO> pluginVOS = new ArrayList<>();
-        pluginVOS.add(pluginVO);
-        final CommonPager<PluginVO> commonPager = new CommonPager<>();
+        List<PluginListVO> pluginVOS = new ArrayList<>();
+        PluginListVO summary = new PluginListVO();
+        summary.setId(pluginVO.getId());
+        summary.setName(pluginVO.getName());
+        pluginVOS.add(summary);
+        final CommonPager<PluginListVO> commonPager = new CommonPager<>();
         commonPager.setPage(pageParameter);
         commonPager.setDataList(pluginVOS);
-        final PluginQuery pluginQuery = new PluginQuery("t_n", 1, 
pageParameter);
-        
given(this.pluginService.listByPage(pluginQuery)).willReturn(commonPager);
+        
given(this.pluginService.listByPage(org.mockito.ArgumentMatchers.any(PluginQuery.class))).willReturn(commonPager);
         this.mockMvc.perform(MockMvcRequestBuilders.get("/plugin-template")
                         .param("name", "t_n")
                         .param("enabled", "1")
@@ -116,9 +119,43 @@ public final class PluginControllerTest {
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.message", 
is(ShenyuResultMessage.QUERY_SUCCESS)))
                 .andExpect(jsonPath("$.data.dataList[0].name", 
is(pluginVO.getName())))
+                .andExpect(jsonPath("$.data.dataList[0].file").doesNotExist())
+                .andExpect(jsonPath("$.data.dataList[0].jar").doesNotExist())
                 .andReturn();
     }
 
+    @Test
+    public void testSearchPluginsOmitsJarField() throws Exception {
+        PluginListVO summary = new PluginListVO();
+        summary.setId("123");
+        summary.setName("t_n");
+        given(pluginService.searchByPage(org.mockito.ArgumentMatchers.any()))
+                .willReturn(new 
com.github.pagehelper.PageInfo<>(Collections.singletonList(summary)));
+        
mockMvc.perform(MockMvcRequestBuilders.post("/plugin-template/list/search")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        
.content("{\"pageNum\":1,\"pageSize\":10,\"condition\":{}}"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.list[0].id", is("123")))
+                .andExpect(jsonPath("$.data.list[0].file").doesNotExist())
+                .andExpect(jsonPath("$.data.list[0].jar").doesNotExist());
+    }
+
+    @Test
+    public void testSearchAdaptorOmitsJarField() throws Exception {
+        PluginListVO summary = new PluginListVO();
+        summary.setId("123");
+        CommonPager<PluginListVO> page = new CommonPager<>();
+        page.setDataList(Collections.singletonList(summary));
+        
given(pluginService.searchByPageToPager(org.mockito.ArgumentMatchers.any())).willReturn(page);
+        
mockMvc.perform(MockMvcRequestBuilders.post("/plugin-template/list/search/adaptor")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        
.content("{\"pageNum\":1,\"pageSize\":10,\"condition\":{}}"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.dataList[0].id", is("123")))
+                .andExpect(jsonPath("$.data.dataList[0].file").doesNotExist())
+                .andExpect(jsonPath("$.data.dataList[0].jar").doesNotExist());
+    }
+
     @Test
     public void testQueryAllPlugins() throws Exception {
         given(this.pluginService.listAll())
@@ -130,11 +167,13 @@ public final class PluginControllerTest {
 
     @Test
     public void testDetailPlugin() throws Exception {
+        pluginVO.setFile("AQID");
         given(this.pluginService.findById("123")).willReturn(pluginVO);
         
this.mockMvc.perform(MockMvcRequestBuilders.get("/plugin-template/{id}", "123"))
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.message", 
is(ShenyuResultMessage.DETAIL_SUCCESS)))
                 .andExpect(jsonPath("$.data.id", is(pluginVO.getId())))
+                .andExpect(jsonPath("$.data.file", is("AQID")))
                 .andReturn();
     }
 
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/AdminQueryIndexTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/AdminQueryIndexTest.java
index c362d9c6f6..c785319ddb 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/AdminQueryIndexTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/AdminQueryIndexTest.java
@@ -56,6 +56,8 @@ class AdminQueryIndexTest extends 
AbstractSpringIntegrationTest {
 
     private static Stream<Arguments> indexes() {
         return Stream.of(
+                Arguments.of("selector", "idx_selector_plugin_id", 
"plugin_id"),
+                Arguments.of("permission", "idx_permission_resource_id", 
"resource_id"),
                 Arguments.of("selector", "idx_selector_ns_plugin_name", 
"namespace_id,plugin_id,selector_name"),
                 Arguments.of("rule", "idx_rule_ns_selector_name", 
"namespace_id,selector_id,rule_name"),
                 Arguments.of("meta_data", "idx_metadata_path_ns", 
"path,namespace_id"),
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/PluginIndexUpgradeContractTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/PluginIndexUpgradeContractTest.java
new file mode 100644
index 0000000000..de958bb889
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/PluginIndexUpgradeContractTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+
+package org.apache.shenyu.admin.mapper;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Guards this PR's index declarations in every supported 2.7.1 to 2.7.2 
upgrade script.
+ * This is a script contract check, not execution on native database engines.
+ */
+class PluginIndexUpgradeContractTest {
+
+    @ParameterizedTest
+    @ValueSource(strings = {"mysql", "ob", "pg", "og", "oracle"})
+    void testUpgradeIndexDeclarations(final String dialect) throws Exception {
+        Path root = Paths.get(System.getProperty("user.dir")).toAbsolutePath();
+        while (Objects.nonNull(root) && 
!Files.isDirectory(root.resolve("db/upgrade"))) {
+            root = root.getParent();
+        }
+        assertNotNull(root, "Repository upgrade scripts must be available to 
the test");
+        Path script = root.resolve("db/upgrade/2.7.1-upgrade-2.7.2-" + dialect 
+ ".sql");
+        String sql = new String(Files.readAllBytes(script), 
StandardCharsets.UTF_8)
+                .replaceAll("(?m)^\\s*--[^\\r\\n]*", "").replace("`", 
"").replace("\"", "").toLowerCase(Locale.ROOT);
+        List<String> statements = Arrays.stream(sql.split(";"))
+                .map(value -> value.trim().replaceAll("\\s+", " "))
+                .collect(Collectors.toList());
+        Map<String, String> expected = expectedIndexes(dialect);
+        for (Map.Entry<String, String> index : expected.entrySet()) {
+            List<String> declarations = statements.stream()
+                    .filter(value -> value.matches("(?s).*\\b" + 
index.getKey() + "\\b.*"))
+                    .collect(Collectors.toList());
+            assertEquals(List.of(index.getValue()), declarations, dialect + ": 
" + index.getKey());
+        }
+        long actualCount = statements.stream().filter(value -> 
value.contains("idx_selector_plugin_id")
+                || value.contains("idx_permission_object_id") || 
value.contains("idx_permission_resource_id")
+                || value.contains("idx_resource_parent_id") || 
value.contains("idx_user_role_user_id")).count();
+        assertEquals(expected.size(), actualCount, "No duplicate or unexpected 
plugin-query index declarations");
+    }
+
+    private Map<String, String> expectedIndexes(final String dialect) {
+        Map<String, String> indexes = new LinkedHashMap<>();
+        addIndex(indexes, dialect, "selector", "idx_selector_plugin_id", 
"plugin_id");
+        addIndex(indexes, dialect, "permission", "idx_permission_resource_id", 
"resource_id");
+        if (!"mysql".equals(dialect)) {
+            addIndex(indexes, dialect, "permission", 
"idx_permission_object_id", "object_id");
+            addIndex(indexes, dialect, "resource", "idx_resource_parent_id", 
"parent_id");
+            addIndex(indexes, dialect, "user_role", "idx_user_role_user_id", 
"user_id");
+        }
+        return indexes;
+    }
+
+    private void addIndex(final Map<String, String> indexes, final String 
dialect,
+                          final String table, final String name, final String 
column) {
+        final String statement;
+        if ("mysql".equals(dialect) || "ob".equals(dialect)) {
+            statement = "alter table " + table + " add index " + name + " (" + 
column + ") using btree";
+        } else if ("pg".equals(dialect) || "og".equals(dialect)) {
+            statement = "create index if not exists " + name + " on public." + 
table + " using btree (" + column + ")";
+        } else {
+            statement = "create index " + name + " on " + table + " (" + 
column + ")";
+        }
+        indexes.put(name, statement);
+    }
+}
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/PluginMapperTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/PluginMapperTest.java
index cd77188ec9..b884dea5f4 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/PluginMapperTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/PluginMapperTest.java
@@ -20,10 +20,15 @@ package org.apache.shenyu.admin.mapper;
 import org.apache.shenyu.admin.AbstractSpringIntegrationTest;
 import org.apache.shenyu.admin.model.dto.PluginDTO;
 import org.apache.shenyu.admin.model.entity.PluginDO;
+import org.apache.shenyu.admin.model.page.PageParameter;
 import org.apache.shenyu.admin.model.query.PluginQuery;
+import org.apache.shenyu.admin.model.vo.PluginListVO;
+import org.apache.shenyu.admin.model.vo.PluginVO;
+import org.apache.shenyu.admin.service.PluginService;
 import org.junit.jupiter.api.Test;
 
 import jakarta.annotation.Resource;
+import java.util.Base64;
 import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
@@ -32,7 +37,9 @@ import java.util.stream.Stream;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
 
 /**
  * Test cases for PluginMapper.
@@ -42,6 +49,9 @@ public final class PluginMapperTest extends 
AbstractSpringIntegrationTest {
     @Resource
     private PluginMapper pluginMapper;
 
+    @Resource
+    private PluginService pluginService;
+
     @Test
     public void selectById() {
         final PluginDTO pluginDTO = buildPluginDTO();
@@ -235,6 +245,45 @@ public final class PluginMapperTest extends 
AbstractSpringIntegrationTest {
         assertThat(deleteResult, equalTo(1));
     }
 
+    @Test
+    public void pagedListOmitsJarWhileDetailAndExportPreserveIt() {
+        final PluginDO plugin = PluginDO.buildPluginDO(buildPluginDTO());
+        final byte[] jar = new byte[] {0, 1, 2, 127, -1};
+        plugin.setPluginJar(jar);
+        assertEquals(1, pluginMapper.insert(plugin));
+        try {
+            final PluginQuery query = new PluginQuery(plugin.getName(), null, 
new PageParameter(1, 10));
+            final List<PluginDO> rows = pluginMapper.selectByQuery(query);
+            assertEquals(1, rows.size());
+            assertNull(rows.get(0).getPluginJar());
+            final List<PluginListVO> page = 
pluginService.listByPage(query).getDataList();
+            assertEquals(1, page.size());
+            assertEquals(plugin.getId(), page.get(0).getId());
+            final com.fasterxml.jackson.databind.JsonNode listJson =
+                    new 
com.fasterxml.jackson.databind.ObjectMapper().valueToTree(page.get(0));
+            org.junit.jupiter.api.Assertions.assertFalse(listJson.has("file"));
+            org.junit.jupiter.api.Assertions.assertFalse(listJson.has("jar"));
+            assertEquals(plugin.getName(), listJson.get("name").asText());
+            final org.apache.shenyu.admin.model.query.PluginQueryCondition 
condition =
+                    new 
org.apache.shenyu.admin.model.query.PluginQueryCondition();
+            condition.setKeyword(plugin.getName());
+            final List<PluginListVO> searched = 
pluginService.searchByCondition(condition);
+            assertEquals(1, searched.size());
+            assertEquals(plugin.getId(), searched.get(0).getId());
+            final com.fasterxml.jackson.databind.JsonNode searchJson =
+                    new 
com.fasterxml.jackson.databind.ObjectMapper().valueToTree(searched.get(0));
+            
org.junit.jupiter.api.Assertions.assertFalse(searchJson.has("file"));
+            
org.junit.jupiter.api.Assertions.assertFalse(searchJson.has("jar"));
+            assertArrayEquals(jar, 
pluginMapper.selectById(plugin.getId()).getPluginJar());
+            assertArrayEquals(jar, 
Base64.getDecoder().decode(pluginService.findById(plugin.getId()).getFile()));
+            final PluginVO exported = pluginService.listAllData().stream()
+                    .filter(item -> 
plugin.getId().equals(item.getId())).findFirst().orElseThrow();
+            assertArrayEquals(jar, 
Base64.getDecoder().decode(exported.getFile()));
+        } finally {
+            pluginMapper.delete(plugin.getId());
+        }
+    }
+
     private PluginDTO buildPluginDTO() {
         final PluginDTO pluginDTO = new PluginDTO();
         pluginDTO.setEnabled(true);
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/PluginServiceTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/PluginServiceTest.java
index 2579e15597..4c25be16bb 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/PluginServiceTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/PluginServiceTest.java
@@ -29,6 +29,7 @@ import org.apache.shenyu.admin.model.page.CommonPager;
 import org.apache.shenyu.admin.model.page.PageParameter;
 import org.apache.shenyu.admin.model.query.PluginQuery;
 import org.apache.shenyu.admin.model.result.ConfigImportResult;
+import org.apache.shenyu.admin.model.vo.PluginListVO;
 import org.apache.shenyu.admin.model.vo.PluginVO;
 import org.apache.shenyu.admin.service.impl.PluginServiceImpl;
 import org.apache.shenyu.admin.service.publish.PluginEventPublisher;
@@ -162,7 +163,7 @@ public final class PluginServiceTest {
         PluginQuery pluginQuery = new PluginQuery("sofa", 1, pageParameter);
         List<PluginDO> pluginDOList = IntStream.range(0, 10).mapToObj(i -> 
buildPluginDO(String.valueOf(i))).collect(Collectors.toList());
         
given(this.pluginMapper.selectByQuery(pluginQuery)).willReturn(pluginDOList);
-        final CommonPager<PluginVO> pluginDOCommonPager = 
this.pluginService.listByPage(pluginQuery);
+        final CommonPager<PluginListVO> pluginDOCommonPager = 
this.pluginService.listByPage(pluginQuery);
         assertEquals(pluginDOCommonPager.getDataList().size(), 
pluginDOList.size());
     }
 

Reply via email to