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());
}