This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 668b59f8b fix: align demo SQL with the numeric schema (#2527)
668b59f8b is described below
commit 668b59f8bcdf680c6bb9f9cd15a3ab3b8e6e8027
Author: xdz997 <[email protected]>
AuthorDate: Sat Aug 22 15:32:27 2026 +0800
fix: align demo SQL with the numeric schema (#2527)
* fix: align demo SQL with numeric schema
* fix: preserve logical instance references in demo metadata
---
README.md | 4 +-
deploy/README.md | 13 +-
deploy/mysql/upgrade-demo-acl.sql | 207 ++++++++-----
deploy/mysql/upgrade-demo-alert.sql | 20 +-
deploy/mysql/upgrade-demo-instance.sql | 339 +++++++++++++--------
.../persistence/DemoDataSqlCompatibilityTest.java | 147 +++++++++
6 files changed, 519 insertions(+), 211 deletions(-)
diff --git a/README.md b/README.md
index 635ac4991..4ebe26507 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,9 @@ The first command starts the bundled RocketMQ topology and
creates the
is healthy before starting Studio with `docker compose ps` from
`deploy/rocketmq`.
The default schema creates only Studio tables. It does not seed instances,
topics, consumer groups, or ACL
records. Development-only sample data can be imported explicitly from
`deploy/mysql/`; it is not part of the
-default deployment.
+default deployment. Import `upgrade-demo-instance.sql` first and then
`upgrade-demo-acl.sql`; both scripts
+target the current numeric-ID schema and are idempotent. They are sample-data
loaders, not upgrade migrations,
+and should never be imported into a production database.
**Studio ports:** Frontend 6789 (Nginx), Backend 8888 (Spring Boot)
diff --git a/deploy/README.md b/deploy/README.md
index 1713f1eea..f080791eb 100644
--- a/deploy/README.md
+++ b/deploy/README.md
@@ -49,8 +49,17 @@ RocketMQ、Prometheus 和云 API 故障由 Studio 的运行态诊断页面展示
故障触发 Studio 容器反复重启。
默认 schema 只创建 Studio 所需的表,不会写入实例、Topic、消费组或 ACL 示例数据。需要演示数据时,
-请在开发环境中显式导入 `deploy/mysql/upgrade-demo-instance.sql` 和
-`deploy/mysql/upgrade-demo-acl.sql`,不要在生产环境导入这些脚本。
+请先初始化当前 `server/src/main/resources/db/schema.sql`,再在开发环境中按顺序导入:
+
+```bash
+docker exec -i rocketmq-studio-mysql mysql -uroot -pstudio123 rocketmq \
+ < deploy/mysql/upgrade-demo-instance.sql
+docker exec -i rocketmq-studio-mysql mysql -uroot -pstudio123 rocketmq \
+ < deploy/mysql/upgrade-demo-acl.sql
+```
+
+两个脚本均按当前 numeric-ID schema 写入并可重复执行;它们只负责演示数据,不创建或迁移业务表,
+不要在生产环境导入。
## 开启登录保护
`studio.auth.login-required` 默认为 `false`,便于本地开发和演示环境直接访问。共享环境建议在
diff --git a/deploy/mysql/upgrade-demo-acl.sql
b/deploy/mysql/upgrade-demo-acl.sql
index ed9ae1977..148a08b1c 100644
--- a/deploy/mysql/upgrade-demo-acl.sql
+++ b/deploy/mysql/upgrade-demo-acl.sql
@@ -1,90 +1,147 @@
-- deploy/mysql/upgrade-demo-acl.sql
--- 存量 MySQL 数据卷增量迁移(2026-08-03):ACL 规则/用户入库
--- 适用:数据卷已初始化、docker-entrypoint-initdb.d 不会再执行的存量部署。
--- 全新数据卷由 server/src/main/resources/db/schema.sql 直接覆盖,无需本脚本。
--- 幂等:可重复执行。
+-- Development-only sample ACL rules and users for the current Studio schema.
--
--- 用法(远程容器内执行):
+-- Prerequisite: initialize the database with
server/src/main/resources/db/schema.sql first.
+-- Run upgrade-demo-instance.sql first so every scope and cluster name refers
to a visible demo instance.
+-- This script does not create or alter schema objects and must not be used as
a schema migration.
+-- It is safe to rerun: users use schema unique keys and rules use their full
logical identity.
+--
+-- Usage:
-- docker exec -i rocketmq-studio-mysql mysql -uroot -pstudio123 rocketmq <
upgrade-demo-acl.sql
--- 固定连接编码,防止 mysql 客户端以 latin1 解释 UTF-8 字节导致中文双重编码
SET NAMES utf8mb4;
--- 1. ACL 规则表
-CREATE TABLE IF NOT EXISTS rmq_acl_rule (
- id VARCHAR(64) PRIMARY KEY,
- principal VARCHAR(128) NOT NULL,
- resource VARCHAR(255) NOT NULL,
- resource_type VARCHAR(32) COMMENT 'Topic/Group/Cluster',
- resource_pattern VARCHAR(32) COMMENT 'LITERAL/PREFIX',
- actions VARCHAR(128) COMMENT '逗号分隔:PUB/SUB/ALL',
- decision VARCHAR(16) COMMENT 'ALLOW/DENY',
- scope VARCHAR(64) COMMENT '生效范围(集群名/实例 id)',
- acl_version VARCHAR(16) COMMENT '1.0/2.0',
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- INDEX idx_principal (principal)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+-- Rule IDs are numeric auto-increment values. Use logical-key checks instead
of carrying the obsolete
+-- acl-001-style string IDs from the pre-standardization schema.
+INSERT INTO rmq_acl_rule
+ (principal, resource, resource_type, resource_pattern, actions, decision,
scope, acl_version)
+SELECT 'user-order-service', 'order_*', 'Topic', 'PREFIX', 'PUB,SUB', 'ALLOW',
'instance-proxy-1', '2.0'
+WHERE NOT EXISTS (
+ SELECT 1 FROM rmq_acl_rule
+ WHERE principal = 'user-order-service' AND resource = 'order_*' AND
resource_type = 'Topic'
+ AND resource_pattern = 'PREFIX' AND scope = 'instance-proxy-1' AND
acl_version = '2.0'
+);
+
+INSERT INTO rmq_acl_rule
+ (principal, resource, resource_type, resource_pattern, actions, decision,
scope, acl_version)
+SELECT 'user-payment-service', 'payment_*', 'Topic', 'PREFIX', 'PUB,SUB',
'ALLOW', 'instance-proxy-1', '2.0'
+WHERE NOT EXISTS (
+ SELECT 1 FROM rmq_acl_rule
+ WHERE principal = 'user-payment-service' AND resource = 'payment_*' AND
resource_type = 'Topic'
+ AND resource_pattern = 'PREFIX' AND scope = 'instance-proxy-1' AND
acl_version = '2.0'
+);
--- 2. ACL 用户表(secret_key 为 base64 编码后的密码,禁止明文存储)
-CREATE TABLE IF NOT EXISTS rmq_acl_user (
- id VARCHAR(64) PRIMARY KEY,
- username VARCHAR(128) NOT NULL,
- access_key VARCHAR(255) NOT NULL,
- secret_key VARCHAR(512) NOT NULL COMMENT 'base64 编码的密码',
- admin TINYINT(1) DEFAULT 0,
- clusters VARCHAR(1024) COMMENT '逗号分隔的集群/实例 id',
- white_remote_address VARCHAR(255) COMMENT 'plain access 账号 IP 白名单,空表示不限制',
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- UNIQUE KEY uk_username (username)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+INSERT INTO rmq_acl_rule
+ (principal, resource, resource_type, resource_pattern, actions, decision,
scope, acl_version)
+SELECT 'user-admin', '*', 'Cluster', 'LITERAL', 'ALL', 'ALLOW',
'instance-proxy-1', '2.0'
+WHERE NOT EXISTS (
+ SELECT 1 FROM rmq_acl_rule
+ WHERE principal = 'user-admin' AND resource = '*' AND resource_type =
'Cluster'
+ AND resource_pattern = 'LITERAL' AND scope = 'instance-proxy-1' AND
acl_version = '2.0'
+);
--- 2.1 已建表的数据卷补列(幂等):rmq_acl_user.white_remote_address
-SET @schema_name := DATABASE();
-SET @white_remote_address_column_exists := (
- SELECT COUNT(*)
- FROM information_schema.columns
- WHERE table_schema = @schema_name
- AND table_name = 'rmq_acl_user'
- AND column_name = 'white_remote_address'
+INSERT INTO rmq_acl_rule
+ (principal, resource, resource_type, resource_pattern, actions, decision,
scope, acl_version)
+SELECT 'user-log-collector', 'audit_operation_log', 'Topic', 'LITERAL', 'SUB',
'ALLOW', 'instance-direct-2', '1.0'
+WHERE NOT EXISTS (
+ SELECT 1 FROM rmq_acl_rule
+ WHERE principal = 'user-log-collector' AND resource = 'audit_operation_log'
AND resource_type = 'Topic'
+ AND resource_pattern = 'LITERAL' AND scope = 'instance-direct-2' AND
acl_version = '1.0'
);
-SET @white_remote_address_sql := IF(@white_remote_address_column_exists = 0,
- "ALTER TABLE rmq_acl_user ADD COLUMN white_remote_address VARCHAR(255)
COMMENT 'plain access 账号 IP 白名单,空表示不限制' AFTER clusters",
- 'SELECT 1');
-PREPARE white_remote_address_statement FROM @white_remote_address_sql;
-EXECUTE white_remote_address_statement;
-DEALLOCATE PREPARE white_remote_address_statement;
--- 3. 规则种子(与 schema.sql 一致)
-INSERT IGNORE INTO rmq_acl_rule
- (id, principal, resource, resource_type, resource_pattern, actions,
decision, scope, acl_version)
-VALUES
- ('acl-001', 'user-order-service', 'order_*',
'Topic', 'PREFIX', 'PUB,SUB', 'ALLOW', 'instance-proxy-1', '2.0'),
- ('acl-002', 'user-payment-service', 'payment_*',
'Topic', 'PREFIX', 'PUB,SUB', 'ALLOW', 'instance-proxy-1', '2.0'),
- ('acl-003', 'user-admin', '*',
'Cluster', 'LITERAL', 'ALL', 'ALLOW', 'instance-proxy-1', '2.0'),
- ('acl-004', 'user-log-collector', 'audit_operation_log',
'Topic', 'LITERAL', 'SUB', 'ALLOW', 'instance-direct-2', '1.0'),
- ('acl-005', 'user-order-service', 'GID_fulfillment_*',
'Group', 'PREFIX', 'SUB', 'ALLOW', 'instance-proxy-1', '2.0'),
- ('acl-006', 'user-inventory-service',
'inventory_deduct_command','Topic', 'LITERAL', 'PUB,SUB', 'ALLOW',
'instance-proxy-1', '2.0'),
- ('acl-007', 'user-guest', 'payment_result_notify',
'Topic', 'LITERAL', 'PUB,SUB', 'DENY', 'instance-proxy-1', '1.0'),
- ('acl-008', 'user-notification-service', 'sms_send_command',
'Topic', 'LITERAL', 'PUB', 'ALLOW', 'instance-proxy-2', '2.0'),
- ('acl-009', 'user-risk-control', 'risk_event_alert',
'Topic', 'LITERAL', 'SUB', 'ALLOW', 'instance-direct-2', '1.0'),
- ('acl-010', 'user-guest', '*',
'Cluster', 'LITERAL', 'PUB', 'DENY', 'instance-proxy-2', '2.0'),
- ('acl-011', 'user-payment-service', 'GID_payment_*',
'Group', 'PREFIX', 'SUB', 'ALLOW', 'instance-proxy-1', '2.0'),
- ('acl-012', 'user-monitor', 'user_behavior_log',
'Topic', 'LITERAL', 'SUB', 'ALLOW', 'instance-proxy-3', '1.0'),
- ('acl-013', 'user-ai-service', 'click_stream_etl',
'Topic', 'LITERAL', 'PUB,SUB', 'ALLOW', 'instance-proxy-3', '2.0');
+INSERT INTO rmq_acl_rule
+ (principal, resource, resource_type, resource_pattern, actions, decision,
scope, acl_version)
+SELECT 'user-order-service', 'GID_fulfillment_*', 'Group', 'PREFIX', 'SUB',
'ALLOW', 'instance-proxy-1', '2.0'
+WHERE NOT EXISTS (
+ SELECT 1 FROM rmq_acl_rule
+ WHERE principal = 'user-order-service' AND resource = 'GID_fulfillment_*'
AND resource_type = 'Group'
+ AND resource_pattern = 'PREFIX' AND scope = 'instance-proxy-1' AND
acl_version = '2.0'
+);
+
+INSERT INTO rmq_acl_rule
+ (principal, resource, resource_type, resource_pattern, actions, decision,
scope, acl_version)
+SELECT 'user-inventory-service', 'inventory_deduct_command', 'Topic',
'LITERAL', 'PUB,SUB', 'ALLOW', 'instance-proxy-1', '2.0'
+WHERE NOT EXISTS (
+ SELECT 1 FROM rmq_acl_rule
+ WHERE principal = 'user-inventory-service' AND resource =
'inventory_deduct_command' AND resource_type = 'Topic'
+ AND resource_pattern = 'LITERAL' AND scope = 'instance-proxy-1' AND
acl_version = '2.0'
+);
+
+INSERT INTO rmq_acl_rule
+ (principal, resource, resource_type, resource_pattern, actions, decision,
scope, acl_version)
+SELECT 'user-guest', 'payment_result_notify', 'Topic', 'LITERAL', 'PUB,SUB',
'DENY', 'instance-proxy-1', '1.0'
+WHERE NOT EXISTS (
+ SELECT 1 FROM rmq_acl_rule
+ WHERE principal = 'user-guest' AND resource = 'payment_result_notify' AND
resource_type = 'Topic'
+ AND resource_pattern = 'LITERAL' AND scope = 'instance-proxy-1' AND
acl_version = '1.0'
+);
+
+INSERT INTO rmq_acl_rule
+ (principal, resource, resource_type, resource_pattern, actions, decision,
scope, acl_version)
+SELECT 'user-notification-service', 'sms_send_command', 'Topic', 'LITERAL',
'PUB', 'ALLOW', 'instance-proxy-2', '2.0'
+WHERE NOT EXISTS (
+ SELECT 1 FROM rmq_acl_rule
+ WHERE principal = 'user-notification-service' AND resource =
'sms_send_command' AND resource_type = 'Topic'
+ AND resource_pattern = 'LITERAL' AND scope = 'instance-proxy-2' AND
acl_version = '2.0'
+);
+
+INSERT INTO rmq_acl_rule
+ (principal, resource, resource_type, resource_pattern, actions, decision,
scope, acl_version)
+SELECT 'user-risk-control', 'risk_event_alert', 'Topic', 'LITERAL', 'SUB',
'ALLOW', 'instance-direct-2', '1.0'
+WHERE NOT EXISTS (
+ SELECT 1 FROM rmq_acl_rule
+ WHERE principal = 'user-risk-control' AND resource = 'risk_event_alert' AND
resource_type = 'Topic'
+ AND resource_pattern = 'LITERAL' AND scope = 'instance-direct-2' AND
acl_version = '1.0'
+);
+
+INSERT INTO rmq_acl_rule
+ (principal, resource, resource_type, resource_pattern, actions, decision,
scope, acl_version)
+SELECT 'user-guest', '*', 'Cluster', 'LITERAL', 'PUB', 'DENY',
'instance-proxy-2', '2.0'
+WHERE NOT EXISTS (
+ SELECT 1 FROM rmq_acl_rule
+ WHERE principal = 'user-guest' AND resource = '*' AND resource_type =
'Cluster'
+ AND resource_pattern = 'LITERAL' AND scope = 'instance-proxy-2' AND
acl_version = '2.0'
+);
+
+INSERT INTO rmq_acl_rule
+ (principal, resource, resource_type, resource_pattern, actions, decision,
scope, acl_version)
+SELECT 'user-payment-service', 'GID_payment_*', 'Group', 'PREFIX', 'SUB',
'ALLOW', 'instance-proxy-1', '2.0'
+WHERE NOT EXISTS (
+ SELECT 1 FROM rmq_acl_rule
+ WHERE principal = 'user-payment-service' AND resource = 'GID_payment_*' AND
resource_type = 'Group'
+ AND resource_pattern = 'PREFIX' AND scope = 'instance-proxy-1' AND
acl_version = '2.0'
+);
+
+INSERT INTO rmq_acl_rule
+ (principal, resource, resource_type, resource_pattern, actions, decision,
scope, acl_version)
+SELECT 'user-monitor', 'user_behavior_log', 'Topic', 'LITERAL', 'SUB',
'ALLOW', 'instance-proxy-3', '1.0'
+WHERE NOT EXISTS (
+ SELECT 1 FROM rmq_acl_rule
+ WHERE principal = 'user-monitor' AND resource = 'user_behavior_log' AND
resource_type = 'Topic'
+ AND resource_pattern = 'LITERAL' AND scope = 'instance-proxy-3' AND
acl_version = '1.0'
+);
+
+INSERT INTO rmq_acl_rule
+ (principal, resource, resource_type, resource_pattern, actions, decision,
scope, acl_version)
+SELECT 'user-ai-service', 'click_stream_etl', 'Topic', 'LITERAL', 'PUB,SUB',
'ALLOW', 'instance-proxy-3', '2.0'
+WHERE NOT EXISTS (
+ SELECT 1 FROM rmq_acl_rule
+ WHERE principal = 'user-ai-service' AND resource = 'click_stream_etl' AND
resource_type = 'Topic'
+ AND resource_pattern = 'LITERAL' AND scope = 'instance-proxy-3' AND
acl_version = '2.0'
+);
--- 4. 用户种子(secret_key 为密码的 base64 编码)
+-- User IDs are numeric auto-increment values. Access keys and usernames are
unique in the
+-- canonical schema, so INSERT IGNORE preserves credentials an operator has
already changed.
INSERT IGNORE INTO rmq_acl_user
- (id, username, access_key, secret_key, admin, clusters)
+ (username, access_key, secret_key, admin, clusters)
VALUES
- ('u-001', 'user-admin', 'AKSTUDIOadmin0001',
'QWRtaW5AU3R1ZGlvIzIwMjY=', 1,
+ ('user-admin', 'AKSTUDIOadmin0001', 'QWRtaW5AU3R1ZGlvIzIwMjY=', 1,
'instance-proxy-1,instance-proxy-2,instance-proxy-3,instance-direct-1,instance-direct-2'),
- ('u-002', 'user-order-service', 'AKSTUDIOordr0002',
'T3JkZXJTdmNAMjAyNiNQcm9k', 0, 'instance-proxy-1'),
- ('u-003', 'user-payment-service', 'AKSTUDIOpaym0003',
'UGF5U3ZjQDIwMjYjUHJvZA==', 0, 'instance-proxy-1'),
- ('u-004', 'user-log-collector', 'AKSTUDIOlogs0004',
'TG9nQ29sbGVjdEAyMDI2I09wcw==', 0, 'instance-direct-2'),
- ('u-005', 'user-guest', 'AKSTUDIOgues0005',
'R3Vlc3RAMjAyNiNSZWFk', 0, 'instance-proxy-2'),
- ('u-006', 'user-inventory-service', 'AKSTUDIOinvn0006',
'SW52U3ZjQDIwMjYjUHJvZA==', 0, 'instance-proxy-1'),
- ('u-007', 'user-notification-service', 'AKSTUDIONtfy0007',
'Tm90aWZ5U3ZjQDIwMjYjTXNn', 0, 'instance-proxy-2'),
- ('u-008', 'user-monitor', 'AKSTUDIOmonr0008',
'TW9uaXRvckAyMDI2I09icw==', 0,
+ ('user-order-service', 'AKSTUDIOordr0002', 'T3JkZXJTdmNAMjAyNiNQcm9k', 0,
'instance-proxy-1'),
+ ('user-payment-service', 'AKSTUDIOpaym0003', 'UGF5U3ZjQDIwMjYjUHJvZA==', 0,
'instance-proxy-1'),
+ ('user-log-collector', 'AKSTUDIOlogs0004', 'TG9nQ29sbGVjdEAyMDI2I09wcw==',
0, 'instance-direct-2'),
+ ('user-guest', 'AKSTUDIOgues0005', 'R3Vlc3RAMjAyNiNSZWFk', 0,
'instance-proxy-2'),
+ ('user-inventory-service', 'AKSTUDIOinvn0006', 'SW52U3ZjQDIwMjYjUHJvZA==',
0, 'instance-proxy-1'),
+ ('user-notification-service', 'AKSTUDIONtfy0007',
'Tm90aWZ5U3ZjQDIwMjYjTXNn', 0, 'instance-proxy-2'),
+ ('user-monitor', 'AKSTUDIOmonr0008', 'TW9uaXRvckAyMDI2I09icw==', 0,
'instance-proxy-3,instance-direct-2');
diff --git a/deploy/mysql/upgrade-demo-alert.sql
b/deploy/mysql/upgrade-demo-alert.sql
index de524ade4..e946b0852 100644
--- a/deploy/mysql/upgrade-demo-alert.sql
+++ b/deploy/mysql/upgrade-demo-alert.sql
@@ -1,7 +1,7 @@
-- deploy/mysql/upgrade-demo-alert.sql
--- 存量 MySQL 数据卷增量迁移:告警规则与系统告警入库(rmq_alert_rule / rmq_system_alert)
--- 适用:数据卷已初始化、docker-entrypoint-initdb.d 不会再执行的存量部署。
--- 全新数据卷由 server/src/main/resources/db/schema.sql 直接覆盖,无需本脚本。
+-- Compatibility helper for deployments that predate the alert tables.
+-- Existing deployments must otherwise follow the canonical schema in
+-- server/src/main/resources/db/schema.sql. This helper only creates missing
alert tables.
-- 幂等:可重复执行。
--
-- 用法(远程容器内执行):
@@ -11,7 +11,9 @@
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS rmq_alert_rule (
- id VARCHAR(64) PRIMARY KEY,
+ `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
+ `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP COMMENT '修改时间',
name VARCHAR(128) NOT NULL,
metric VARCHAR(128),
operator VARCHAR(16),
@@ -25,19 +27,19 @@ CREATE TABLE IF NOT EXISTS rmq_alert_rule (
broker_name VARCHAR(128),
cluster_name VARCHAR(128),
severity VARCHAR(32),
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+ PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS rmq_system_alert (
- id VARCHAR(64) PRIMARY KEY,
+ `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
+ `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP COMMENT '修改时间',
level VARCHAR(32),
title VARCHAR(255),
description TEXT,
time DATETIME,
acknowledged TINYINT(1) DEFAULT 0,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
INDEX idx_level (level),
INDEX idx_acknowledged (acknowledged)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/deploy/mysql/upgrade-demo-instance.sql
b/deploy/mysql/upgrade-demo-instance.sql
index 493bdba7e..240203973 100644
--- a/deploy/mysql/upgrade-demo-instance.sql
+++ b/deploy/mysql/upgrade-demo-instance.sql
@@ -1,132 +1,223 @@
-- deploy/mysql/upgrade-demo-instance.sql
--- 存量 MySQL 数据卷增量迁移(2026-08-03):实例持久化 + topic/group 增加 instance_id
--- 适用:数据卷已初始化、docker-entrypoint-initdb.d 不会再执行的存量部署。
--- 全新数据卷由 server/src/main/resources/db/schema.sql 直接覆盖,无需本脚本。
--- 幂等:可重复执行。
+-- Development-only sample instances, topics and consumer groups for the
current Studio schema.
--
--- 用法(远程容器内执行):
+-- Prerequisite: initialize the database with
server/src/main/resources/db/schema.sql first.
+-- This script does not create or alter schema objects and must not be used as
a schema migration.
+-- It is safe to rerun: instance names and (cluster_id, instance_id, name)
metadata keys are unique.
+--
+-- Usage:
-- docker exec -i rocketmq-studio-mysql mysql -uroot -pstudio123 rocketmq <
upgrade-demo-instance.sql
--- 固定连接编码,防止 mysql 客户端以 latin1 解释 UTF-8 字节导致中文双重编码
SET NAMES utf8mb4;
--- 1. 实例注册表
-CREATE TABLE IF NOT EXISTS rmq_instance (
- id VARCHAR(64) PRIMARY KEY,
- name VARCHAR(128) NOT NULL,
- remark VARCHAR(255),
- type VARCHAR(32) NOT NULL COMMENT 'PROXY/PROXY_LOCAL/PROXY_CLUSTER/DIRECT',
- endpoint VARCHAR(512) NOT NULL,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-
--- 2. rmq_topic / rmq_group 增加 instance_id 列与索引(MySQL 8.0 无 ADD COLUMN IF NOT
EXISTS,用 information_schema 判断)
-SET @sql = (SELECT IF(
- COUNT(*) > 0, 'SELECT ''rmq_topic.instance_id already exists'' AS msg',
- 'ALTER TABLE rmq_topic ADD COLUMN instance_id VARCHAR(64) COMMENT
''归属实例,引用 rmq_instance.id'' AFTER cluster_id, ADD INDEX idx_instance
(instance_id)')
- FROM information_schema.COLUMNS
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'rmq_topic' AND COLUMN_NAME
= 'instance_id');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
-SET @sql = (SELECT IF(
- COUNT(*) > 0, 'SELECT ''rmq_group.instance_id already exists'' AS msg',
- 'ALTER TABLE rmq_group ADD COLUMN instance_id VARCHAR(64) COMMENT
''归属实例,引用 rmq_instance.id'' AFTER cluster_id, ADD INDEX idx_instance
(instance_id)')
- FROM information_schema.COLUMNS
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'rmq_group' AND COLUMN_NAME
= 'instance_id');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- 3. 默认 5 个实例(幂等)
-INSERT IGNORE INTO rmq_instance (id, name, remark, type, endpoint) VALUES
- ('instance-direct-1', 'instance-direct-1', '直连实例 1,交易核心链路(NameServer 直连)',
'DIRECT', '10.0.1.11:9876'),
- ('instance-direct-2', 'instance-direct-2', '直连实例 2,风控与审计链路(NameServer 直连)',
'DIRECT', '10.0.1.12:9876'),
- ('instance-proxy-1', 'instance-proxy-1', 'Proxy 实例 1,电商交易主链路',
'PROXY_CLUSTER', '10.0.2.21:8080'),
- ('instance-proxy-2', 'instance-proxy-2', 'Proxy 实例 2,营销与会员链路',
'PROXY_CLUSTER', '10.0.2.22:8080'),
- ('instance-proxy-3', 'instance-proxy-3', 'Proxy 实例 3,物流与大数据链路',
'PROXY_CLUSTER', '10.0.2.23:8080');
-
--- 4. 旧种子数据回填 instance_id(旧部署里这 9 个 topic、8 个 group 已存在,INSERT IGNORE 不会更新它们)
-UPDATE rmq_topic SET instance_id = 'instance-proxy-1'
- WHERE instance_id IS NULL AND name IN (
- 'order_create_event', 'order_status_change', 'order_timeout_cancel',
- 'payment_result_notify', 'inventory_deduct_command');
-UPDATE rmq_topic SET instance_id = 'instance-proxy-2'
- WHERE instance_id IS NULL AND name IN ('marketing_coupon_issue');
-UPDATE rmq_topic SET instance_id = 'instance-proxy-3'
- WHERE instance_id IS NULL AND name IN ('logistics_tracking_update',
'settlement_daily_archive');
-UPDATE rmq_topic SET instance_id = 'instance-direct-2'
- WHERE instance_id IS NULL AND name IN ('risk_control_audit');
-
-UPDATE rmq_group SET instance_id = 'instance-proxy-1'
- WHERE instance_id IS NULL AND name IN (
- 'GID_fulfillment_order', 'GID_inventory_deduct', 'GID_payment_result');
-UPDATE rmq_group SET instance_id = 'instance-proxy-2'
- WHERE instance_id IS NULL AND name IN ('GID_marketing_coupon');
-UPDATE rmq_group SET instance_id = 'instance-proxy-3'
- WHERE instance_id IS NULL AND name IN (
- 'GID_logistics_tracking', 'GID_settlement_archive',
'GID_bi_realtime_report');
-UPDATE rmq_group SET instance_id = 'instance-direct-2'
- WHERE instance_id IS NULL AND name IN ('studio-trace-consumer');
-
--- 5. 补充新种子 topic/group(与 schema.sql 一致,已存在的行被 IGNORE 跳过)
-INSERT IGNORE INTO rmq_topic
- (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+-- The application owns numeric auto-increment primary keys. The stable API
identifier is
+-- rmq_instance.name, so demo rows omit id and resolve child instance_id
values by name.
+INSERT IGNORE INTO rmq_instance
+ (name, remark, type, endpoint, vendor)
VALUES
- ('rocketmq-studio', 'instance-proxy-1', 'refund_apply_event',
'NORMAL', 4, 4, 6,
- '退款申请事件,客服与财务系统订阅', 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-1', 'cart_sync_event',
'NORMAL', 4, 4, 6,
- '购物车多端同步事件', 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-1', 'trade_close_archive',
'NORMAL', 2, 2, 4,
- '交易关单归档,只读供对账回溯', 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-2', 'marketing_campaign_push',
'NORMAL', 8, 8, 6,
- '大促活动 push 触达,按人群包分批投递', 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-2', 'member_register_event',
'NORMAL', 4, 4, 6,
- '新会员注册事件,积分与权益系统订阅', 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-2', 'member_points_change', 'FIFO',
4, 4, 6,
- '会员积分变动,按会员 ID 分区保序', 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-2', 'member_level_upgrade',
'DELAY', 4, 4, 6,
- '会员升级权益延迟发放', 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-2', 'sms_send_command',
'NORMAL', 8, 8, 6,
- '短信下发指令,网关限流后消费', 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-3', 'logistics_dispatch_order', 'FIFO',
8, 8, 6,
- '运单调度指令,同单有序', 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-3', 'bi_realtime_report',
'NORMAL', 16, 16, 6,
- '实时报表数据流,BI 大屏消费', 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-3', 'user_behavior_log',
'NORMAL', 16, 16, 6,
- '用户行为埋点日志,离线分析入湖', 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-3', 'click_stream_etl',
'NORMAL', 8, 8, 6,
- '点击流 ETL 中间结果', 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-direct-1', 'trade_core_order_flow', 'FIFO',
8, 8, 6,
- '交易核心订单流水,直连低延迟链路', 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-direct-1', 'payment_channel_callback',
'NORMAL', 8, 8, 6,
- '支付渠道回调通知', 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-direct-1', 'account_ledger_entry',
'TRANSACTION', 8, 8, 6,
- '账户记账分录,与账务落库同事务', 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-direct-1', 'ledger_reconcile_task',
'DELAY', 4, 4, 6,
- '对账任务延迟触发,T+1 凌晨执行', 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-direct-2', 'risk_event_alert',
'NORMAL', 4, 4, 6,
- '风控命中事件告警,实时推送处置平台', 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-direct-2', 'audit_operation_log',
'NORMAL', 8, 8, 6,
- '操作审计日志,合规留存 180 天', 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-direct-2', 'compliance_report_daily',
'DELAY', 2, 2, 6,
- '合规日报延迟生成任务', 'ACTIVE', 'seed');
-
-INSERT IGNORE INTO rmq_group
+ ('instance-direct-1', '直连实例 1,交易核心链路(NameServer 直连)', 'DIRECT',
'10.0.1.11:9876', 'APACHE'),
+ ('instance-direct-2', '直连实例 2,风控与审计链路(NameServer 直连)', 'DIRECT',
'10.0.1.12:9876', 'APACHE'),
+ ('instance-proxy-1', 'Proxy 实例 1,电商交易主链路', 'PROXY_CLUSTER',
'10.0.2.21:8080', 'APACHE'),
+ ('instance-proxy-2', 'Proxy 实例 2,营销与会员链路', 'PROXY_CLUSTER',
'10.0.2.22:8080', 'APACHE'),
+ ('instance-proxy-3', 'Proxy 实例 3,物流与大数据链路', 'PROXY_CLUSTER',
'10.0.2.23:8080', 'APACHE');
+
+-- Topic metadata keeps the stable instance name in instance_id, matching the
application contract.
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'refund_apply_event', 'NORMAL', 4, 4, 6,
+ '退款申请事件,客服与财务系统订阅', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-1';
+
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'cart_sync_event', 'NORMAL', 4, 4, 6,
+ '购物车多端同步事件', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-1';
+
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'trade_close_archive', 'NORMAL', 2, 2, 4,
+ '交易关单归档,只读供对账回溯', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-1';
+
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'marketing_campaign_push', 'NORMAL', 8, 8, 6,
+ '大促活动 push 触达,按人群包分批投递', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-2';
+
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'member_register_event', 'NORMAL', 4, 4, 6,
+ '新会员注册事件,积分与权益系统订阅', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-2';
+
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'member_points_change', 'FIFO', 4, 4, 6,
+ '会员积分变动,按会员 ID 分区保序', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-2';
+
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'member_level_upgrade', 'DELAY', 4, 4, 6,
+ '会员升级权益延迟发放', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-2';
+
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'sms_send_command', 'NORMAL', 8, 8, 6,
+ '短信下发指令,网关限流后消费', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-2';
+
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'logistics_dispatch_order', 'FIFO', 8, 8, 6,
+ '运单调度指令,同单有序', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-3';
+
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'bi_realtime_report', 'NORMAL', 16, 16, 6,
+ '实时报表数据流,BI 大屏消费', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-3';
+
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'user_behavior_log', 'NORMAL', 16, 16, 6,
+ '用户行为埋点日志,离线分析入湖', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-3';
+
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'click_stream_etl', 'NORMAL', 8, 8, 6,
+ '点击流 ETL 中间结果', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-3';
+
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'trade_core_order_flow', 'FIFO', 8, 8, 6,
+ '交易核心订单流水,直连低延迟链路', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-direct-1';
+
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'payment_channel_callback', 'NORMAL', 8, 8, 6,
+ '支付渠道回调通知', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-direct-1';
+
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'account_ledger_entry', 'TRANSACTION', 8, 8, 6,
+ '账户记账分录,与账务落库同事务', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-direct-1';
+
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'ledger_reconcile_task', 'DELAY', 4, 4, 6,
+ '对账任务延迟触发,T+1 凌晨执行', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-direct-1';
+
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'risk_event_alert', 'NORMAL', 4, 4, 6,
+ '风控命中事件告警,实时推送处置平台', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-direct-2';
+
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'audit_operation_log', 'NORMAL', 8, 8, 6,
+ '操作审计日志,合规留存 180 天', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-direct-2';
+
+INSERT IGNORE INTO rmq_instance_topic
+ (cluster_id, instance_id, name, topic_type, read_queue_nums,
write_queue_nums, perm, remark, status, created_by)
+SELECT 'rocketmq-studio', name, 'compliance_report_daily', 'DELAY', 2, 2, 6,
+ '合规日报延迟生成任务', 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-direct-2';
+
+-- Consumer-group metadata uses the same stable-name reference.
+INSERT IGNORE INTO rmq_instance_group
(cluster_id, instance_id, name, consume_type, message_model, max_retry,
status, created_by)
-VALUES
- ('rocketmq-studio', 'instance-proxy-1', 'GID_refund_process', 'PUSH',
'CLUSTERING', 8, 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-1', 'GID_cart_sync', 'PUSH',
'CLUSTERING', 3, 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-1', 'GID_trade_archive', 'PULL',
'CLUSTERING', 3, 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-2', 'GID_campaign_push', 'PUSH',
'CLUSTERING', 3, 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-2', 'GID_member_points', 'PUSH',
'CLUSTERING', 16, 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-2', 'GID_member_benefit', 'PUSH',
'CLUSTERING', 8, 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-2', 'GID_sms_gateway', 'PUSH',
'CLUSTERING', 5, 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-3', 'GID_logistics_dispatch', 'PUSH',
'CLUSTERING', 16, 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-3', 'GID_behavior_ingest', 'PUSH',
'CLUSTERING', 3, 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-proxy-3', 'GID_click_stream_etl', 'PUSH',
'CLUSTERING', 3, 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-direct-1', 'GID_trade_core_flow', 'PUSH',
'CLUSTERING', 16, 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-direct-1', 'GID_pay_channel_cb', 'PUSH',
'CLUSTERING', 16, 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-direct-1', 'GID_ledger_entry', 'PUSH',
'CLUSTERING', 16, 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-direct-1', 'GID_reconcile_task', 'PULL',
'CLUSTERING', 3, 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-direct-2', 'GID_risk_alert', 'PUSH',
'CLUSTERING', 8, 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-direct-2', 'GID_audit_archive', 'PUSH',
'CLUSTERING', 3, 'ACTIVE', 'seed'),
- ('rocketmq-studio', 'instance-direct-2', 'GID_compliance_daily', 'PULL',
'CLUSTERING', 1, 'ACTIVE', 'seed');
+SELECT 'rocketmq-studio', name, 'GID_refund_process', 'PUSH', 'CLUSTERING', 8,
'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-1';
+
+INSERT IGNORE INTO rmq_instance_group
+ (cluster_id, instance_id, name, consume_type, message_model, max_retry,
status, created_by)
+SELECT 'rocketmq-studio', name, 'GID_cart_sync', 'PUSH', 'CLUSTERING', 3,
'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-1';
+
+INSERT IGNORE INTO rmq_instance_group
+ (cluster_id, instance_id, name, consume_type, message_model, max_retry,
status, created_by)
+SELECT 'rocketmq-studio', name, 'GID_trade_archive', 'PULL', 'CLUSTERING', 3,
'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-1';
+
+INSERT IGNORE INTO rmq_instance_group
+ (cluster_id, instance_id, name, consume_type, message_model, max_retry,
status, created_by)
+SELECT 'rocketmq-studio', name, 'GID_campaign_push', 'PUSH', 'CLUSTERING', 3,
'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-2';
+
+INSERT IGNORE INTO rmq_instance_group
+ (cluster_id, instance_id, name, consume_type, message_model, max_retry,
status, created_by)
+SELECT 'rocketmq-studio', name, 'GID_member_points', 'PUSH', 'CLUSTERING', 16,
'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-2';
+
+INSERT IGNORE INTO rmq_instance_group
+ (cluster_id, instance_id, name, consume_type, message_model, max_retry,
status, created_by)
+SELECT 'rocketmq-studio', name, 'GID_member_benefit', 'PUSH', 'CLUSTERING', 8,
'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-2';
+
+INSERT IGNORE INTO rmq_instance_group
+ (cluster_id, instance_id, name, consume_type, message_model, max_retry,
status, created_by)
+SELECT 'rocketmq-studio', name, 'GID_sms_gateway', 'PUSH', 'CLUSTERING', 5,
'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-2';
+
+INSERT IGNORE INTO rmq_instance_group
+ (cluster_id, instance_id, name, consume_type, message_model, max_retry,
status, created_by)
+SELECT 'rocketmq-studio', name, 'GID_logistics_dispatch', 'PUSH',
'CLUSTERING', 16, 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-3';
+
+INSERT IGNORE INTO rmq_instance_group
+ (cluster_id, instance_id, name, consume_type, message_model, max_retry,
status, created_by)
+SELECT 'rocketmq-studio', name, 'GID_behavior_ingest', 'PUSH', 'CLUSTERING',
3, 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-3';
+
+INSERT IGNORE INTO rmq_instance_group
+ (cluster_id, instance_id, name, consume_type, message_model, max_retry,
status, created_by)
+SELECT 'rocketmq-studio', name, 'GID_click_stream_etl', 'PUSH', 'CLUSTERING',
3, 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-proxy-3';
+
+INSERT IGNORE INTO rmq_instance_group
+ (cluster_id, instance_id, name, consume_type, message_model, max_retry,
status, created_by)
+SELECT 'rocketmq-studio', name, 'GID_trade_core_flow', 'PUSH', 'CLUSTERING',
16, 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-direct-1';
+
+INSERT IGNORE INTO rmq_instance_group
+ (cluster_id, instance_id, name, consume_type, message_model, max_retry,
status, created_by)
+SELECT 'rocketmq-studio', name, 'GID_pay_channel_cb', 'PUSH', 'CLUSTERING',
16, 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-direct-1';
+
+INSERT IGNORE INTO rmq_instance_group
+ (cluster_id, instance_id, name, consume_type, message_model, max_retry,
status, created_by)
+SELECT 'rocketmq-studio', name, 'GID_ledger_entry', 'PUSH', 'CLUSTERING', 16,
'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-direct-1';
+
+INSERT IGNORE INTO rmq_instance_group
+ (cluster_id, instance_id, name, consume_type, message_model, max_retry,
status, created_by)
+SELECT 'rocketmq-studio', name, 'GID_reconcile_task', 'PULL', 'CLUSTERING', 3,
'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-direct-1';
+
+INSERT IGNORE INTO rmq_instance_group
+ (cluster_id, instance_id, name, consume_type, message_model, max_retry,
status, created_by)
+SELECT 'rocketmq-studio', name, 'GID_risk_alert', 'PUSH', 'CLUSTERING', 8,
'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-direct-2';
+
+INSERT IGNORE INTO rmq_instance_group
+ (cluster_id, instance_id, name, consume_type, message_model, max_retry,
status, created_by)
+SELECT 'rocketmq-studio', name, 'GID_audit_archive', 'PUSH', 'CLUSTERING', 3,
'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-direct-2';
+
+INSERT IGNORE INTO rmq_instance_group
+ (cluster_id, instance_id, name, consume_type, message_model, max_retry,
status, created_by)
+SELECT 'rocketmq-studio', name, 'GID_compliance_daily', 'PULL', 'CLUSTERING',
1, 'ACTIVE', 'seed'
+FROM rmq_instance WHERE name = 'instance-direct-2';
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/persistence/DemoDataSqlCompatibilityTest.java
b/server/src/test/java/org/apache/rocketmq/studio/persistence/DemoDataSqlCompatibilityTest.java
new file mode 100644
index 000000000..293024751
--- /dev/null
+++
b/server/src/test/java/org/apache/rocketmq/studio/persistence/DemoDataSqlCompatibilityTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.rocketmq.studio.persistence;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.jdbc.datasource.init.ScriptUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DemoDataSqlCompatibilityTest {
+
+ @Test
+ void demoScriptsMatchCanonicalSchemaAndRemainIdempotent() throws Exception
{
+ try (Connection connection = DriverManager.getConnection(
+
"jdbc:h2:mem:demo-data-sql;MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1",
"sa", "")) {
+ ScriptUtils.executeSqlScript(connection, new
ClassPathResource("db/schema.sql"));
+
+ for (int pass = 0; pass < 2; pass++) {
+ executeDeployScript(connection, "upgrade-demo-instance.sql");
+ executeDeployScript(connection, "upgrade-demo-acl.sql");
+ }
+
+ assertThat(count(connection, "rmq_instance")).isEqualTo(5);
+ assertThat(count(connection, "rmq_instance_topic")).isEqualTo(19);
+ assertThat(count(connection, "rmq_instance_group")).isEqualTo(17);
+ assertThat(count(connection, "rmq_acl_rule")).isEqualTo(13);
+ assertThat(count(connection, "rmq_acl_user")).isEqualTo(8);
+ assertThat(countOrphanedMetadata(connection,
"rmq_instance_topic")).isZero();
+ assertThat(countOrphanedMetadata(connection,
"rmq_instance_group")).isZero();
+ assertThat(countNumericMetadataReferences(connection,
"rmq_instance_topic")).isZero();
+ assertThat(countNumericMetadataReferences(connection,
"rmq_instance_group")).isZero();
+ assertThat(allIdsAreNumeric(connection, "rmq_instance")).isTrue();
+ assertThat(allIdsAreNumeric(connection, "rmq_acl_rule")).isTrue();
+ assertThat(allIdsAreNumeric(connection, "rmq_acl_user")).isTrue();
+ }
+ }
+
+ @Test
+ void alertCompatibilityHelperCreatesCanonicalNumericTables() throws
Exception {
+ try (Connection connection = DriverManager.getConnection(
+
"jdbc:h2:mem:demo-alert-sql;MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1",
"sa", "")) {
+ executeDeployScript(connection, "upgrade-demo-alert.sql");
+ executeDeployScript(connection, "upgrade-demo-alert.sql");
+
+ try (Statement statement = connection.createStatement()) {
+ statement.executeUpdate("INSERT INTO rmq_alert_rule (name,
enabled) VALUES ('demo-rule', TRUE)");
+ statement.executeUpdate("INSERT INTO rmq_system_alert (level,
title, acknowledged)"
+ + " VALUES ('warning', 'demo-alert', FALSE)");
+ }
+
+ assertThat(count(connection, "rmq_alert_rule")).isEqualTo(1);
+ assertThat(count(connection, "rmq_system_alert")).isEqualTo(1);
+ assertThat(allIdsAreNumeric(connection,
"rmq_alert_rule")).isTrue();
+ assertThat(allIdsAreNumeric(connection,
"rmq_system_alert")).isTrue();
+ assertThat(hasColumn(connection, "rmq_alert_rule",
"gmt_create")).isTrue();
+ assertThat(hasColumn(connection, "rmq_alert_rule",
"gmt_modified")).isTrue();
+ assertThat(hasColumn(connection, "rmq_alert_rule",
"created_at")).isFalse();
+ assertThat(hasColumn(connection, "rmq_system_alert",
"updated_at")).isFalse();
+ }
+ }
+
+ private static void executeDeployScript(Connection connection, String
filename) throws IOException {
+ String script = Files.readString(resolveDeployScript(filename),
StandardCharsets.UTF_8)
+ .replace("SET NAMES utf8mb4;", "");
+ ScriptUtils.executeSqlScript(connection,
+ new ByteArrayResource(script.getBytes(StandardCharsets.UTF_8),
filename));
+ }
+
+ private static Path resolveDeployScript(String filename) {
+ Path fromModule = Path.of("..", "deploy", "mysql", filename);
+ if (Files.isRegularFile(fromModule)) {
+ return fromModule;
+ }
+ Path fromRepository = Path.of("deploy", "mysql", filename);
+ assertThat(Files.isRegularFile(fromRepository))
+ .as("deploy SQL script %s must be available from the Maven
working directory", filename)
+ .isTrue();
+ return fromRepository;
+ }
+
+ private static long count(Connection connection, String table) throws
SQLException {
+ try (Statement statement = connection.createStatement();
+ ResultSet result = statement.executeQuery("SELECT COUNT(*) FROM "
+ table)) {
+ result.next();
+ return result.getLong(1);
+ }
+ }
+
+ private static long countOrphanedMetadata(Connection connection, String
table) throws SQLException {
+ try (Statement statement = connection.createStatement();
+ ResultSet result = statement.executeQuery("SELECT COUNT(*) FROM "
+ table
+ + " child LEFT JOIN rmq_instance parent ON parent.name =
child.instance_id"
+ + " WHERE parent.id IS NULL")) {
+ result.next();
+ return result.getLong(1);
+ }
+ }
+
+ private static long countNumericMetadataReferences(Connection connection,
String table) throws SQLException {
+ try (Statement statement = connection.createStatement();
+ ResultSet result = statement.executeQuery("SELECT COUNT(*) FROM "
+ table
+ + " child JOIN rmq_instance parent ON CAST(parent.id AS
VARCHAR) = child.instance_id")) {
+ result.next();
+ return result.getLong(1);
+ }
+ }
+
+ private static boolean allIdsAreNumeric(Connection connection, String
table) throws SQLException {
+ try (Statement statement = connection.createStatement();
+ ResultSet result = statement.executeQuery("SELECT COUNT(*) FROM "
+ table + " WHERE id <= 0")) {
+ result.next();
+ return result.getLong(1) == 0;
+ }
+ }
+
+ private static boolean hasColumn(Connection connection, String table,
String column) throws SQLException {
+ try (ResultSet columns = connection.getMetaData().getColumns(null,
null, table, column)) {
+ return columns.next();
+ }
+ }
+}