diff --git a/frontend/src/components/ConnectionModal.edit-password.test.tsx b/frontend/src/components/ConnectionModal.edit-password.test.tsx
index 1cd1f0a..735b666 100644
--- a/frontend/src/components/ConnectionModal.edit-password.test.tsx
+++ b/frontend/src/components/ConnectionModal.edit-password.test.tsx
@@ -33,10 +33,10 @@ describe('ConnectionModal data source registry', () => {
expect(source).toContain('type === "elasticsearch"');
expect(source).toContain("return '支持索引浏览、Mapping 检查、JSON DSL 和 query_string 查询';");
expect(source).toContain(
- 'type === "clickhouse" ? "default" : (type === "redis" || type === "elasticsearch" || type === "chroma" || type === "qdrant" || type === "mqtt" || type === "kafka" || type === "rabbitmq") ? "" : "root";',
+ 'type === "clickhouse" ? "default" : (type === "redis" || type === "elasticsearch" || type === "chroma" || type === "qdrant" || type === "rocketmq" || type === "mqtt" || type === "kafka" || type === "rabbitmq") ? "" : "root";',
);
expect(source).toContain(
- 'placeholder={(dbType === "elasticsearch" || dbType === "chroma" || dbType === "qdrant" || dbType === "mqtt" || dbType === "kafka" || dbType === "rabbitmq") ? "未开启认证可留空" : undefined}',
+ 'placeholder={(dbType === "elasticsearch" || dbType === "chroma" || dbType === "qdrant" || dbType === "rocketmq" || dbType === "mqtt" || dbType === "kafka" || dbType === "rabbitmq") ? "未开启认证可留空" : undefined}',
);
expect(source).toContain('label="显示数据库 (留空显示全部)"');
});
@@ -77,6 +77,23 @@ describe('ConnectionModal data source registry', () => {
expect(source).toContain('return "fetchSize=1024&timeZone=Asia%2FShanghai";');
});
+ it('exposes RocketMQ in the create-connection picker with nameserver and topic defaults', () => {
+ expect(source).toContain("case 'rocketmq':");
+ expect(source).toContain('return 9876;');
+ expect(source).toContain('rocketmq: ["rocketmq", "rmq"]');
+ expect(source).toContain("key: 'rocketmq'");
+ expect(source).toContain("name: 'RocketMQ'");
+ expect(source).toContain('dbType === "rocketmq"');
+ expect(source).toContain("return 'NameServer / Topic / Consumer Group';");
+ expect(source).toContain('return "rocketmq://accessKey:secretKey@127.0.0.1:9876,127.0.0.2:9876/orders.events?topology=cluster&groupId=gonavi&namespace=prod&tag=TagA&pullBatchSize=32&startOffset=latest";');
+ expect(source).toContain('return "groupId=gonavi&namespace=prod&tag=TagA&pullBatchSize=32&startOffset=latest";');
+ expect(source).toContain('label="默认 Topic(可选)"');
+ expect(source).toContain('label={dbType === "rocketmq" ? "Access Key" : "用户名"}');
+ expect(source).toContain('label={dbType === "rocketmq" ? "Secret Key" : "密码"}');
+ expect(source).toContain('emptyPlaceholder: dbType === "rocketmq" ? "未开启认证可留空" : "密码"');
+ expect(source).toContain('retainedLabel: dbType === "rocketmq" ? "已保存 Secret Key" : "已保存密码"');
+ });
+
it('exposes MQTT in the create-connection picker with broker and topic-filter defaults', () => {
expect(source).toContain("case 'mqtt':");
expect(source).toContain('return 1883;');
diff --git a/frontend/src/components/ConnectionModal.tsx b/frontend/src/components/ConnectionModal.tsx
index e106664..0a65376 100644
--- a/frontend/src/components/ConnectionModal.tsx
+++ b/frontend/src/components/ConnectionModal.tsx
@@ -385,6 +385,7 @@ const ConnectionModal: React.FC<{
);
const disableLocalBackdropFilter = isMacLikePlatform();
const mysqlTopology = Form.useWatch("mysqlTopology", form) || "single";
+ const rocketmqTopology = Form.useWatch("rocketmqTopology", form) || "single";
const mqttTopology = Form.useWatch("mqttTopology", form) || "single";
const kafkaTopology = Form.useWatch("kafkaTopology", form) || "single";
const mongoTopology = Form.useWatch("mongoTopology", form) || "single";
@@ -420,6 +421,7 @@ const ConnectionModal: React.FC<{
);
const isOceanBaseOracle = dbType === "oceanbase" && oceanBaseProtocol === "oracle";
const isMySQLLike = isMySQLCompatibleType(dbType) && !isOceanBaseOracle;
+ const isRocketMQ = dbType === "rocketmq";
const isMQTT = dbType === "mqtt";
const isKafka = dbType === "kafka";
const isRabbitMQ = dbType === "rabbitmq";
@@ -1756,6 +1758,49 @@ const ConnectionModal: React.FC<{
};
}
+ if (type === "rocketmq") {
+ const defaultPort = getDefaultPortByType(type);
+ const parsed =
+ parseMultiHostUri(trimmedUri, "rocketmq") ||
+ parseMultiHostUri(trimmedUri, "rmq");
+ if (!parsed) {
+ return null;
+ }
+ if (!parsed.hosts.length || parsed.hosts.length > MAX_URI_HOSTS) {
+ return null;
+ }
+ if (parsed.hosts.some((entry) => !isValidUriHostEntry(entry))) {
+ return null;
+ }
+ const hostList = normalizeAddressList(parsed.hosts, defaultPort);
+ if (!hostList.length) {
+ return null;
+ }
+ const primary = parseHostPort(
+ hostList[0] || `localhost:${defaultPort}`,
+ defaultPort,
+ );
+ const topology = String(parsed.params.get("topology") || "")
+ .trim()
+ .toLowerCase();
+ const timeoutValue = Number(parsed.params.get("timeout"));
+ return {
+ host: primary?.host || "localhost",
+ port: primary?.port || defaultPort,
+ user: parsed.username,
+ password: parsed.password,
+ database: parsed.database || "",
+ rocketmqTopology:
+ topology === "cluster" || hostList.length > 1 ? "cluster" : "single",
+ rocketmqHosts: hostList.slice(1),
+ connectionParams: serializeConnectionParams(parsed.params),
+ timeout:
+ Number.isFinite(timeoutValue) && timeoutValue > 0
+ ? Math.min(MAX_TIMEOUT_SECONDS, Math.trunc(timeoutValue))
+ : undefined,
+ };
+ }
+
if (type === "rabbitmq") {
const defaultPort = getDefaultPortByType(type);
const parsed = parseSingleHostUri(
@@ -2044,6 +2089,9 @@ const ConnectionModal: React.FC<{
if (dbType === "iotdb") {
return "iotdb://root:root@127.0.0.1:6667/root.sg";
}
+ if (dbType === "rocketmq") {
+ return "rocketmq://accessKey:secretKey@127.0.0.1:9876,127.0.0.2:9876/orders.events?topology=cluster&groupId=gonavi&namespace=prod&tag=TagA&pullBatchSize=32&startOffset=latest";
+ }
if (dbType === "mqtt") {
return "mqtt://user:pass@127.0.0.1:1883/devices%2F%2B%2Ftelemetry?topology=cluster&clientId=gonavi-desktop&qos=1";
}
@@ -2108,6 +2156,8 @@ const ConnectionModal: React.FC<{
return "timezone=Asia%2FShanghai";
case "iotdb":
return "fetchSize=1024&timeZone=Asia%2FShanghai";
+ case "rocketmq":
+ return "groupId=gonavi&namespace=prod&tag=TagA&pullBatchSize=32&startOffset=latest";
case "mqtt":
return "topics=devices%2F%2B%2Ftelemetry,%24SYS%2F%23&clientId=gonavi-desktop&qos=1&cleanSession=true&fetchWaitMs=4000";
case "kafka":
@@ -2236,6 +2286,26 @@ const ConnectionModal: React.FC<{
return `mqtt://${encodedAuth}${allBrokers.join(",")}${topicPath}${query ? `?${query}` : ""}`;
}
+ if (type === "rocketmq") {
+ const primary = toAddress(host, port, defaultPort);
+ const nameservers =
+ values.rocketmqTopology === "cluster"
+ ? normalizeAddressList(values.rocketmqHosts, defaultPort)
+ : [];
+ const allNameServers = normalizeAddressList([primary, ...nameservers], defaultPort);
+ const params = new URLSearchParams();
+ if (allNameServers.length > 1 || values.rocketmqTopology === "cluster") {
+ params.set("topology", "cluster");
+ }
+ if (Number.isFinite(timeout) && timeout > 0) {
+ params.set("timeout", String(timeout));
+ }
+ mergeConnectionParams(params, values.connectionParams);
+ const topicPath = database ? `/${encodeURIComponent(database)}` : "";
+ const query = params.toString();
+ return `rocketmq://${encodedAuth}${allNameServers.join(",")}${topicPath}${query ? `?${query}` : ""}`;
+ }
+
if (type === "rabbitmq") {
const address = toAddress(host, port, defaultPort);
const params = new URLSearchParams();
@@ -2629,6 +2699,8 @@ const ConnectionModal: React.FC<{
configType === "sphinx"
? normalizedHosts.slice(1)
: [];
+ const rocketmqHosts =
+ configType === "rocketmq" ? normalizedHosts.slice(1) : [];
const mqttHosts =
configType === "mqtt" ? normalizedHosts.slice(1) : [];
const kafkaHosts =
@@ -2640,6 +2712,9 @@ const ConnectionModal: React.FC<{
const mysqlIsReplica =
String(config.topology || "").toLowerCase() === "replica" ||
mysqlReplicaHosts.length > 0;
+ const rocketmqIsCluster =
+ String(config.topology || "").toLowerCase() === "cluster" ||
+ rocketmqHosts.length > 0;
const mqttIsCluster =
String(config.topology || "").toLowerCase() === "cluster" ||
mqttHosts.length > 0;
@@ -2718,6 +2793,8 @@ const ConnectionModal: React.FC<{
timeout: resolvedJvmTimeout,
mysqlTopology: mysqlIsReplica ? "replica" : "single",
mysqlReplicaHosts: mysqlReplicaHosts,
+ rocketmqTopology: rocketmqIsCluster ? "cluster" : "single",
+ rocketmqHosts: rocketmqHosts,
mqttTopology: mqttIsCluster ? "cluster" : "single",
mqttHosts: mqttHosts,
kafkaTopology: kafkaIsCluster ? "cluster" : "single",
@@ -3714,6 +3791,23 @@ const ConnectionModal: React.FC<{
}
}
+ if (type === "rocketmq") {
+ const nameservers =
+ mergedValues.rocketmqTopology === "cluster"
+ ? normalizeAddressList(mergedValues.rocketmqHosts, defaultPort)
+ : [];
+ const allHosts = normalizeAddressList(
+ [`${primaryHost}:${primaryPort}`, ...nameservers],
+ defaultPort,
+ );
+ if (mergedValues.rocketmqTopology === "cluster" || allHosts.length > 1) {
+ hosts = allHosts;
+ topology = "cluster";
+ } else {
+ topology = "single";
+ }
+ }
+
if (type === "mongodb") {
mongoSrvEnabled = !!mergedValues.mongoSrv;
const extraHosts =
@@ -3950,6 +4044,7 @@ const ConnectionModal: React.FC<{
includeDatabases: undefined,
includeRedisDatabases: undefined,
mysqlTopology: "single",
+ rocketmqTopology: "single",
mqttTopology: "single",
kafkaTopology: "single",
redisTopology: "single",
@@ -3961,6 +4056,7 @@ const ConnectionModal: React.FC<{
mongoAuthMechanism: "",
savePassword: true,
mysqlReplicaHosts: [],
+ rocketmqHosts: [],
mqttHosts: [],
kafkaHosts: [],
redisHosts: [],
@@ -4016,6 +4112,8 @@ const ConnectionModal: React.FC<{
httpTunnelUser: "",
httpTunnelPassword: "",
mysqlTopology: "single",
+ rocketmqTopology: "single",
+ mqttTopology: "single",
kafkaTopology: "single",
redisTopology: "single",
mongoTopology: "single",
@@ -4026,6 +4124,8 @@ const ConnectionModal: React.FC<{
mongoAuthMechanism: "",
savePassword: true,
mysqlReplicaHosts: [],
+ rocketmqHosts: [],
+ mqttHosts: [],
kafkaHosts: [],
redisHosts: [],
redisSentinelMaster: "",
@@ -4041,7 +4141,7 @@ const ConnectionModal: React.FC<{
});
} else if (type !== "custom") {
const defaultUser =
- type === "clickhouse" ? "default" : (type === "redis" || type === "elasticsearch" || type === "chroma" || type === "qdrant" || type === "mqtt" || type === "kafka" || type === "rabbitmq") ? "" : "root";
+ type === "clickhouse" ? "default" : (type === "redis" || type === "elasticsearch" || type === "chroma" || type === "qdrant" || type === "rocketmq" || type === "mqtt" || type === "kafka" || type === "rabbitmq") ? "" : "root";
const sslCapableType = supportsSSLForType(type);
setUseSSL(false);
setUseHttpTunnel(false);
@@ -4060,6 +4160,7 @@ const ConnectionModal: React.FC<{
httpTunnelUser: "",
httpTunnelPassword: "",
mysqlTopology: "single",
+ rocketmqTopology: "single",
mqttTopology: "single",
kafkaTopology: "single",
redisTopology: "single",
@@ -4071,6 +4172,7 @@ const ConnectionModal: React.FC<{
mongoAuthMechanism: "",
savePassword: true,
mysqlReplicaHosts: [],
+ rocketmqHosts: [],
mqttHosts: [],
kafkaHosts: [],
redisHosts: [],
@@ -5189,6 +5291,22 @@ const ConnectionModal: React.FC<{
),
})}
+ {dbType === "rocketmq" &&
+ renderConfigSectionCard({
+ sectionKey: "service",
+ icon: ,
+ children: (
+
+
+
+ ),
+ })}
+
{dbType === "mqtt" &&
renderConfigSectionCard({
sectionKey: "service",
@@ -5295,6 +5413,28 @@ const ConnectionModal: React.FC<{
}),
})}
+ {isRocketMQ &&
+ renderConfigSectionCard({
+ sectionKey: "connectionMode",
+ icon: ,
+ children: renderChoiceCards({
+ fieldName: "rocketmqTopology",
+ value: String(rocketmqTopology),
+ options: [
+ {
+ value: "single",
+ label: "单 NameServer",
+ description: "只配置一个 NameServer,适合本地或简单环境。",
+ },
+ {
+ value: "cluster",
+ label: "集群模式",
+ description: "配置多个 NameServer,提高路由发现与故障切换成功率。",
+ },
+ ],
+ }),
+ })}
+
{isMQTT &&
renderConfigSectionCard({
sectionKey: "connectionMode",
@@ -5337,6 +5477,26 @@ const ConnectionModal: React.FC<{
),
})}
+ {isRocketMQ &&
+ rocketmqTopology === "cluster" &&
+ renderConfigSectionCard({
+ sectionKey: "replica",
+ icon: ,
+ children: (
+
+
+
+ ),
+ })}
+
{isMQTT &&
mqttTopology === "cluster" &&
renderConfigSectionCard({
@@ -5472,19 +5632,19 @@ const ConnectionModal: React.FC<{
>
-
+
@@ -6402,6 +6562,7 @@ const ConnectionModal: React.FC<{
connectionParams: "",
oceanBaseProtocol: "mysql",
mysqlTopology: "single",
+ rocketmqTopology: "single",
mqttTopology: "single",
kafkaTopology: "single",
redisTopology: "single",
@@ -6411,6 +6572,7 @@ const ConnectionModal: React.FC<{
mongoAuthMechanism: "",
savePassword: true,
mysqlReplicaHosts: [],
+ rocketmqHosts: [],
mqttHosts: [],
kafkaHosts: [],
redisHosts: [],
diff --git a/frontend/src/components/DatabaseIcons.test.tsx b/frontend/src/components/DatabaseIcons.test.tsx
index 9f0b4e6..7cf5f09 100644
--- a/frontend/src/components/DatabaseIcons.test.tsx
+++ b/frontend/src/components/DatabaseIcons.test.tsx
@@ -39,6 +39,13 @@ describe('DatabaseIcons', () => {
expect(markup).toContain('>Io');
});
+ it('includes RocketMQ in the selectable database icons', () => {
+ expect(DB_ICON_TYPES).toContain('rocketmq');
+ expect(getDbIconLabel('rocketmq')).toBe('RocketMQ');
+ const markup = renderToStaticMarkup(<>{getDbIcon('rocketmq', undefined, 22)}>);
+ expect(markup).toContain('>Rm');
+ });
+
it('includes MQTT in the selectable database icons', () => {
expect(DB_ICON_TYPES).toContain('mqtt');
expect(getDbIconLabel('mqtt')).toBe('MQTT');
diff --git a/frontend/src/components/DatabaseIcons.tsx b/frontend/src/components/DatabaseIcons.tsx
index bf55eb3..e842906 100644
--- a/frontend/src/components/DatabaseIcons.tsx
+++ b/frontend/src/components/DatabaseIcons.tsx
@@ -52,6 +52,7 @@ const DB_DEFAULT_COLORS: Record = {
iris: '#1F6FEB',
tdengine: '#2962FF',
iotdb: '#0F766E',
+ rocketmq: '#EA580C',
mqtt: '#0EA5A4',
kafka: '#F97316',
rabbitmq: '#FF6B35',
@@ -195,6 +196,9 @@ const TDengineIcon: React.FC = ({ size = 16, color }) => (
const IoTDBIcon: React.FC = ({ size = 16, color }) => (
);
+const RocketMQIcon: React.FC = ({ size = 16, color }) => (
+
+);
const MQTTIcon: React.FC = ({ size = 16, color }) => (
);
@@ -266,6 +270,7 @@ const DB_ICON_MAP: Record> = {
iris: IrisIcon,
tdengine: TDengineIcon,
iotdb: IoTDBIcon,
+ rocketmq: RocketMQIcon,
mqtt: MQTTIcon,
kafka: KafkaIcon,
rabbitmq: RabbitMQIcon,
@@ -279,7 +284,7 @@ const DB_ICON_MAP: Record> = {
export const DB_ICON_TYPES: string[] = [
'mysql', 'mariadb', 'oceanbase', 'postgres', 'redis', 'mongodb', 'jvm',
'oracle', 'sqlserver', 'sqlite', 'duckdb', 'clickhouse', 'starrocks',
- 'kingbase', 'dameng', 'vastbase', 'opengauss', 'gaussdb', 'goldendb', 'highgo', 'iris', 'tdengine', 'iotdb', 'mqtt', 'kafka', 'rabbitmq', 'chroma', 'qdrant', 'elasticsearch', 'custom',
+ 'kingbase', 'dameng', 'vastbase', 'opengauss', 'gaussdb', 'goldendb', 'highgo', 'iris', 'tdengine', 'iotdb', 'rocketmq', 'mqtt', 'kafka', 'rabbitmq', 'chroma', 'qdrant', 'elasticsearch', 'custom',
];
/** 该类型是否有品牌 SVG 文件 */
@@ -301,7 +306,7 @@ export const getDbIconLabel = (type: string): string => {
sqlserver: 'SQL Server', clickhouse: 'ClickHouse', sqlite: 'SQLite',
starrocks: 'StarRocks',
duckdb: 'DuckDB', kingbase: '金仓', dameng: '达梦',
- vastbase: 'VastBase', opengauss: 'OpenGauss', gaussdb: 'GaussDB', goldendb: 'GoldenDB', highgo: '瀚高', iris: 'InterSystems IRIS', tdengine: 'TDengine', iotdb: 'Apache IoTDB', mqtt: 'MQTT', kafka: 'Kafka', rabbitmq: 'RabbitMQ',
+ vastbase: 'VastBase', opengauss: 'OpenGauss', gaussdb: 'GaussDB', goldendb: 'GoldenDB', highgo: '瀚高', iris: 'InterSystems IRIS', tdengine: 'TDengine', iotdb: 'Apache IoTDB', rocketmq: 'RocketMQ', mqtt: 'MQTT', kafka: 'Kafka', rabbitmq: 'RabbitMQ',
chroma: 'Chroma',
qdrant: 'Qdrant',
elasticsearch: 'Elasticsearch',
diff --git a/frontend/src/components/MessagePublishModal.tsx b/frontend/src/components/MessagePublishModal.tsx
index e9b379f..85571f2 100644
--- a/frontend/src/components/MessagePublishModal.tsx
+++ b/frontend/src/components/MessagePublishModal.tsx
@@ -14,6 +14,28 @@ import {
const { Text } = Typography;
const { TextArea } = Input;
+const ROCKETMQ_DELAY_LEVEL_OPTIONS = [
+ { label: '不延时', value: 0 },
+ { label: '1 · 1s', value: 1 },
+ { label: '2 · 5s', value: 2 },
+ { label: '3 · 10s', value: 3 },
+ { label: '4 · 30s', value: 4 },
+ { label: '5 · 1m', value: 5 },
+ { label: '6 · 2m', value: 6 },
+ { label: '7 · 3m', value: 7 },
+ { label: '8 · 4m', value: 8 },
+ { label: '9 · 5m', value: 9 },
+ { label: '10 · 6m', value: 10 },
+ { label: '11 · 7m', value: 11 },
+ { label: '12 · 8m', value: 12 },
+ { label: '13 · 9m', value: 13 },
+ { label: '14 · 10m', value: 14 },
+ { label: '15 · 20m', value: 15 },
+ { label: '16 · 30m', value: 16 },
+ { label: '17 · 1h', value: 17 },
+ { label: '18 · 2h', value: 18 },
+];
+
export type MessagePublishModalProps = {
open: boolean;
connection: SavedConnection | null;
@@ -171,22 +193,48 @@ const MessagePublishModal: React.FC = ({
)}
+ {presentation.showTag && (
+
+
+
+ )}
+
+ {presentation.showDelayLevel && (
+
+
+
+ )}
+
{presentation.showKey && (
-
-
-
-
-
+
+ {presentation.showKeyMode ? (
+
+
+
+
+
+
+
+
+ ) : (
-
+
-
+ )}
)}
@@ -208,13 +256,15 @@ const MessagePublishModal: React.FC = ({
-
-
-
+ {presentation.showHeaders && (
+
+
+
+ )}
{presentation.showProperties && (
{
if (normalized === 'elastic') return 'elasticsearch';
if (normalized === 'chromadb' || normalized === 'chroma-db') return 'chroma';
if (normalized === 'qdrantdb' || normalized === 'qdrant-db') return 'qdrant';
+ if (normalized === 'rocket-mq' || normalized === 'rocket_mq' || normalized === 'apache-rocketmq' || normalized === 'apache_rocketmq' || normalized === 'rmq') return 'rocketmq';
if (normalized === 'apache-iotdb' || normalized === 'apache_iotdb') return 'iotdb';
if (normalized === 'mqtts') return 'mqtt';
if (normalized === 'apache-kafka' || normalized === 'apache_kafka') return 'kafka';
diff --git a/frontend/src/utils/connectionModalPresentation.ts b/frontend/src/utils/connectionModalPresentation.ts
index 3bfc3be..97d1897 100644
--- a/frontend/src/utils/connectionModalPresentation.ts
+++ b/frontend/src/utils/connectionModalPresentation.ts
@@ -297,6 +297,20 @@ export const resolveConnectionConfigLayout = (
],
};
}
+ if (type === 'rocketmq') {
+ return {
+ kind: 'generic-sql',
+ sections: [
+ 'identity',
+ 'uri',
+ 'target',
+ 'connectionMode',
+ 'replica',
+ 'service',
+ 'credentials',
+ ],
+ };
+ }
if (type === 'kafka') {
return {
kind: 'generic-sql',
diff --git a/frontend/src/utils/connectionTypeCapabilities.ts b/frontend/src/utils/connectionTypeCapabilities.ts
index c38f28d..d001cd4 100644
--- a/frontend/src/utils/connectionTypeCapabilities.ts
+++ b/frontend/src/utils/connectionTypeCapabilities.ts
@@ -16,6 +16,7 @@ export const singleHostUriSchemesByType: Record = {
elasticsearch: ["http", "https"],
chroma: ["http", "https", "chroma"],
qdrant: ["http", "https", "qdrant"],
+ rocketmq: ["rocketmq", "rmq"],
mqtt: ["mqtt", "mqtts", "tcp", "ssl", "tls"],
rabbitmq: ["rabbitmq", "http", "https"],
};
@@ -30,6 +31,12 @@ const normalizeConnectionType = (type: string) =>
case "greatdb":
case "gdb":
return "goldendb";
+ case "rocket-mq":
+ case "rocket_mq":
+ case "apache-rocketmq":
+ case "apache_rocketmq":
+ case "rmq":
+ return "rocketmq";
case "mqtts":
return "mqtt";
default:
@@ -167,6 +174,7 @@ export const supportsConnectionParamsForType = (type: string) =>
type === "elasticsearch" ||
type === "chroma" ||
type === "qdrant" ||
+ type === "rocketmq" ||
type === "mqtt" ||
type === "kafka" ||
type === "rabbitmq";
diff --git a/frontend/src/utils/connectionTypeCatalog.ts b/frontend/src/utils/connectionTypeCatalog.ts
index 8cbd997..696f34b 100644
--- a/frontend/src/utils/connectionTypeCatalog.ts
+++ b/frontend/src/utils/connectionTypeCatalog.ts
@@ -64,6 +64,7 @@ export const CONNECTION_TYPE_GROUPS: ConnectionTypeCatalogGroup[] = [
{
label: '消息队列',
items: [
+ { key: 'rocketmq', name: 'RocketMQ' },
{ key: 'mqtt', name: 'MQTT' },
{ key: 'kafka', name: 'Kafka' },
{ key: 'rabbitmq', name: 'RabbitMQ' },
@@ -124,6 +125,8 @@ export const getConnectionTypeDefaultPort = (type: string): number => {
return 8000;
case 'qdrant':
return 6333;
+ case 'rocketmq':
+ return 9876;
case 'mqtt':
return 1883;
case 'kafka':
@@ -162,6 +165,8 @@ export const getConnectionTypeHint = (type: string): string => {
return 'Collection 浏览、向量搜索和 Payload 过滤';
case 'iotdb':
return 'Storage Group / Device / Timeseries';
+ case 'rocketmq':
+ return 'NameServer / Topic / Consumer Group';
case 'mqtt':
return 'Broker / Topic Filter / QoS';
case 'kafka':
diff --git a/frontend/src/utils/dataSourceCapabilities.test.ts b/frontend/src/utils/dataSourceCapabilities.test.ts
index da63fa3..2166f69 100644
--- a/frontend/src/utils/dataSourceCapabilities.test.ts
+++ b/frontend/src/utils/dataSourceCapabilities.test.ts
@@ -164,6 +164,28 @@ describe('dataSourceCapabilities', () => {
});
});
+ it('treats RocketMQ as a queryable messaging datasource with manual total count and publish support', () => {
+ expect(getDataSourceCapabilities({ type: 'rocketmq' })).toMatchObject({
+ type: 'rocketmq',
+ supportsQueryEditor: true,
+ supportsSqlQueryExport: false,
+ supportsCopyInsert: false,
+ supportsCreateDatabase: false,
+ supportsRenameDatabase: false,
+ supportsDropDatabase: false,
+ supportsMessagePublish: true,
+ forceReadOnlyQueryResult: true,
+ preferManualTotalCount: true,
+ });
+ expect(getDataSourceCapabilities({ type: 'custom', driver: 'rmq' })).toMatchObject({
+ type: 'rocketmq',
+ supportsQueryEditor: true,
+ supportsMessagePublish: true,
+ forceReadOnlyQueryResult: true,
+ preferManualTotalCount: true,
+ });
+ });
+
it('treats MQTT as a queryable messaging datasource with manual total count and publish support', () => {
expect(getDataSourceCapabilities({ type: 'mqtt' })).toMatchObject({
type: 'mqtt',
diff --git a/frontend/src/utils/dataSourceCapabilities.ts b/frontend/src/utils/dataSourceCapabilities.ts
index 3234997..cbbc8d1 100644
--- a/frontend/src/utils/dataSourceCapabilities.ts
+++ b/frontend/src/utils/dataSourceCapabilities.ts
@@ -35,6 +35,13 @@ const normalizeDataSourceToken = (raw: string): string => {
case 'qdrantdb':
case 'qdrant-db':
return 'qdrant';
+ case 'rocketmq':
+ case 'rocket-mq':
+ case 'rocket_mq':
+ case 'apache-rocketmq':
+ case 'apache_rocketmq':
+ case 'rmq':
+ return 'rocketmq';
case 'mqtt':
case 'mqtts':
return 'mqtt';
@@ -124,9 +131,9 @@ const COPY_INSERT_TYPES = new Set([
]);
const QUERY_EDITOR_DISABLED_TYPES = new Set(['redis']);
-const FORCE_READ_ONLY_QUERY_TYPES = new Set(['tdengine', 'iotdb', 'clickhouse', 'mqtt', 'kafka', 'rabbitmq']);
-const MESSAGE_PUBLISH_TYPES = new Set(['mqtt', 'kafka', 'rabbitmq']);
-const MANUAL_TOTAL_COUNT_TYPES = new Set(['duckdb', 'oracle', 'mqtt']);
+const FORCE_READ_ONLY_QUERY_TYPES = new Set(['tdengine', 'iotdb', 'clickhouse', 'rocketmq', 'mqtt', 'kafka', 'rabbitmq']);
+const MESSAGE_PUBLISH_TYPES = new Set(['rocketmq', 'mqtt', 'kafka', 'rabbitmq']);
+const MANUAL_TOTAL_COUNT_TYPES = new Set(['duckdb', 'oracle', 'rocketmq', 'mqtt']);
const APPROXIMATE_TABLE_COUNT_TYPES = new Set(['duckdb', 'oracle']);
const APPROXIMATE_TOTAL_PAGE_TYPES = new Set(['duckdb']);
diff --git a/frontend/src/utils/messagePublish.test.ts b/frontend/src/utils/messagePublish.test.ts
index 94b32db..92ee5f4 100644
--- a/frontend/src/utils/messagePublish.test.ts
+++ b/frontend/src/utils/messagePublish.test.ts
@@ -128,4 +128,45 @@ describe('messagePublish', () => {
bodyMode: 'json',
});
});
+
+ it('builds a RocketMQ publish JSON command with tag, keys and delay level', () => {
+ const result = buildMessagePublishCommand(
+ { type: 'rocketmq' },
+ {
+ destination: 'orders.events',
+ key: 'key-a,key-b',
+ tag: 'TagA',
+ delayLevel: 3,
+ bodyMode: 'json',
+ body: '{"id":1,"event":"created"}',
+ properties: '{"trace":"trace-1"}',
+ },
+ );
+
+ const command = JSON.parse(result.commandText);
+ expect(result.transportLabel).toBe('RocketMQ Topic');
+ expect(result.destinationLabel).toBe('orders.events');
+ expect(command).toMatchObject({
+ publish: 'orders.events',
+ tag: 'TagA',
+ delayLevel: 3,
+ properties: {
+ trace: 'trace-1',
+ },
+ });
+ expect(command.keys).toEqual(['key-a', 'key-b']);
+ expect(command.payload).toMatchObject({ id: 1, event: 'created' });
+ });
+
+ it('seeds RocketMQ default publish draft with connection tag and delay level defaults', () => {
+ expect(createDefaultMessagePublishDraft(
+ { type: 'rocketmq', database: 'orders.events', connectionParams: 'tag=TagA&delayLevel=5' },
+ '',
+ )).toMatchObject({
+ destination: 'orders.events',
+ tag: 'TagA',
+ delayLevel: 5,
+ bodyMode: 'json',
+ });
+ });
});
diff --git a/frontend/src/utils/messagePublish.ts b/frontend/src/utils/messagePublish.ts
index b789a8a..d1334ef 100644
--- a/frontend/src/utils/messagePublish.ts
+++ b/frontend/src/utils/messagePublish.ts
@@ -17,6 +17,8 @@ export type MessagePublishDraft = {
routingKey?: string;
qos?: number;
retain?: boolean;
+ tag?: string;
+ delayLevel?: number;
keyMode?: MessagePublishValueMode;
key?: string;
bodyMode?: MessagePublishValueMode;
@@ -39,9 +41,16 @@ export type MessagePublishPresentation = {
alertMessage: string;
successHint: string;
showKey: boolean;
+ showKeyMode: boolean;
+ keyLabel: string;
+ keyPlaceholder: string;
showExchange: boolean;
showRoutingKey: boolean;
+ showHeaders: boolean;
showProperties: boolean;
+ showTag: boolean;
+ tagPlaceholder: string;
+ showDelayLevel: boolean;
showQos: boolean;
showRetain: boolean;
};
@@ -142,6 +151,9 @@ const resolveDefaultDestination = (config: ConnectionLike, explicitDestination:
if (resolvedType === 'kafka') {
return String(config?.database || '').trim();
}
+ if (resolvedType === 'rocketmq') {
+ return String(config?.database || params.get('defaultTopic') || params.get('topic') || '').trim();
+ }
if (resolvedType === 'mqtt') {
return String(config?.database || params.get('defaultTopic') || params.get('topic') || '').trim();
}
@@ -165,9 +177,40 @@ export const getMessagePublishPresentation = (
alertMessage: '当前表单会自动拼装 RabbitMQ publish JSON 命令,并通过 Management API 执行测试发送。',
successHint: '留空 Exchange 时会使用默认交换机并按 Queue 名作为 routing key。',
showKey: false,
+ showKeyMode: false,
+ keyLabel: '消息 Key(可选)',
+ keyPlaceholder: '',
showExchange: true,
showRoutingKey: true,
+ showHeaders: true,
showProperties: true,
+ showTag: false,
+ tagPlaceholder: '',
+ showDelayLevel: false,
+ showQos: false,
+ showRetain: false,
+ };
+ }
+
+ if (resolvedType === 'rocketmq') {
+ return {
+ transportLabel: 'RocketMQ Topic',
+ destinationLabel: 'Topic',
+ destinationPlaceholder: '例如:orders.events',
+ destinationRequiredMessage: '请输入 Topic',
+ alertMessage: '当前表单会自动拼装 RocketMQ publish JSON 命令,并通过 NameServer/Broker 执行测试发送。',
+ successHint: 'Tag、Keys、Delay Level 与 Properties 会一并写入 RocketMQ 消息属性。',
+ showKey: true,
+ showKeyMode: false,
+ keyLabel: '消息 Keys(可选)',
+ keyPlaceholder: '可输入多个 Key,使用逗号分隔',
+ showExchange: false,
+ showRoutingKey: false,
+ showHeaders: false,
+ showProperties: true,
+ showTag: true,
+ tagPlaceholder: '例如:TagA',
+ showDelayLevel: true,
showQos: false,
showRetain: false,
};
@@ -182,9 +225,16 @@ export const getMessagePublishPresentation = (
alertMessage: '当前表单会自动拼装 MQTT publish JSON 命令,并直接通过 broker 执行测试发送。',
successHint: 'QoS 与 retain 可单独指定;未填写时沿用当前连接中的默认参数。',
showKey: false,
+ showKeyMode: false,
+ keyLabel: '消息 Key(可选)',
+ keyPlaceholder: '',
showExchange: false,
showRoutingKey: false,
+ showHeaders: false,
showProperties: false,
+ showTag: false,
+ tagPlaceholder: '',
+ showDelayLevel: false,
showQos: true,
showRetain: true,
};
@@ -198,9 +248,16 @@ export const getMessagePublishPresentation = (
alertMessage: '当前表单会自动拼装 Kafka publish JSON 命令,并直接调用后端执行测试发送。',
successHint: 'Headers 会作为 Kafka Record Headers 一并发送。',
showKey: true,
+ showKeyMode: true,
+ keyLabel: '消息 Key(可选)',
+ keyPlaceholder: '可留空;JSON 模式请输入一行合法 JSON',
showExchange: false,
showRoutingKey: false,
+ showHeaders: true,
showProperties: false,
+ showTag: false,
+ tagPlaceholder: '',
+ showDelayLevel: false,
showQos: false,
showRetain: false,
};
@@ -226,6 +283,20 @@ export const createDefaultMessagePublishDraft = (
};
}
+ if (resolvedType === 'rocketmq') {
+ const delayLevel = Number(params.get('delayLevel') || params.get('delay_level'));
+ return {
+ destination: resolvedDestination,
+ tag: String(params.get('tag') || params.get('tags') || '').trim(),
+ delayLevel: Number.isFinite(delayLevel) && delayLevel > 0 ? Math.trunc(delayLevel) : undefined,
+ key: '',
+ bodyMode: 'json',
+ body: '{\n "event": "test",\n "source": "gonavi"\n}',
+ headers: '',
+ properties: '{\n "x-source": "gonavi"\n}',
+ };
+ }
+
if (resolvedType === 'mqtt') {
const qosValue = Number(params.get('qos'));
return {
@@ -279,6 +350,43 @@ export const buildMessagePublishCommand = (
};
}
+ if (resolvedType === 'rocketmq') {
+ const bodyMode = normalizeMode(draft.bodyMode, 'json');
+ const command: Record = {
+ publish: destination,
+ payload: parseRequiredPayload(draft.body, bodyMode, '消息体'),
+ };
+
+ const keys = String(draft.key || '')
+ .split(/[,;|\s,]+/g)
+ .map((item) => item.trim())
+ .filter(Boolean);
+ if (keys.length > 0) {
+ command.keys = keys;
+ }
+
+ const tag = String(draft.tag || '').trim();
+ if (tag) {
+ command.tag = tag;
+ }
+
+ const delayLevel = Number(draft.delayLevel);
+ if (Number.isFinite(delayLevel) && delayLevel > 0) {
+ command.delayLevel = Math.trunc(delayLevel);
+ }
+
+ const properties = parseOptionalJSONObject(draft.properties, 'Properties');
+ if (properties && Object.keys(properties).length > 0) {
+ command.properties = properties;
+ }
+
+ return {
+ commandText: JSON.stringify(command, null, 2),
+ destinationLabel: destination,
+ transportLabel: 'RocketMQ Topic',
+ };
+ }
+
if (resolvedType === 'rabbitmq') {
const params = resolveConnectionParams(config);
const bodyMode = normalizeMode(draft.bodyMode, 'json');
diff --git a/frontend/src/utils/objectQueryTemplates.test.ts b/frontend/src/utils/objectQueryTemplates.test.ts
index f7abe21..bfcdeda 100644
--- a/frontend/src/utils/objectQueryTemplates.test.ts
+++ b/frontend/src/utils/objectQueryTemplates.test.ts
@@ -7,6 +7,10 @@ describe('buildTableSelectQuery', () => {
expect(buildTableSelectQuery('postgres', 'public.MyTable')).toBe('SELECT * FROM public."MyTable";');
});
+ it('adds a preview limit for RocketMQ topic browsing', () => {
+ expect(buildTableSelectQuery('rocketmq', 'orders.events')).toBe('SELECT * FROM "orders.events" LIMIT 100;');
+ });
+
it('adds a preview limit for Kafka topic browsing', () => {
expect(buildTableSelectQuery('kafka', 'logs.app-1')).toBe('SELECT * FROM "logs.app-1" LIMIT 100;');
});
diff --git a/frontend/src/utils/objectQueryTemplates.ts b/frontend/src/utils/objectQueryTemplates.ts
index a8589da..b4468f8 100644
--- a/frontend/src/utils/objectQueryTemplates.ts
+++ b/frontend/src/utils/objectQueryTemplates.ts
@@ -5,7 +5,7 @@ export const buildTableSelectQuery = (dbType: string, tableName: string): string
if (!normalizedTableName) {
return 'SELECT * FROM ';
}
- if (['mqtt', 'kafka', 'rabbitmq'].includes(String(dbType || '').trim().toLowerCase())) {
+ if (['rocketmq', 'mqtt', 'kafka', 'rabbitmq'].includes(String(dbType || '').trim().toLowerCase())) {
return `SELECT * FROM ${quoteQualifiedIdent(dbType, normalizedTableName)} LIMIT 100;`;
}
return `SELECT * FROM ${quoteQualifiedIdent(dbType, normalizedTableName)};`;
diff --git a/frontend/src/utils/sql.test.ts b/frontend/src/utils/sql.test.ts
index f72b2cb..4368c96 100644
--- a/frontend/src/utils/sql.test.ts
+++ b/frontend/src/utils/sql.test.ts
@@ -59,6 +59,11 @@ describe('quoteQualifiedIdent', () => {
.toBe('`root`.`sg`.`d1`');
});
+ it('keeps RocketMQ topic names as one quoted identifier', () => {
+ expect(quoteQualifiedIdent('rocketmq', 'orders.events.v1'))
+ .toBe('"orders.events.v1"');
+ });
+
it('keeps MQTT topic filters as one quoted identifier', () => {
expect(quoteQualifiedIdent('mqtt', 'devices/+/telemetry.v1'))
.toBe('"devices/+/telemetry.v1"');
diff --git a/frontend/src/utils/sql.ts b/frontend/src/utils/sql.ts
index 2376d95..6d9449c 100644
--- a/frontend/src/utils/sql.ts
+++ b/frontend/src/utils/sql.ts
@@ -54,7 +54,7 @@ export const quoteIdentPart = (dbType: string, ident: string) => {
export const quoteQualifiedIdent = (dbType: string, ident: string) => {
const raw = (ident || '').trim();
if (!raw) return raw;
- if (['mqtt', 'kafka', 'rabbitmq'].includes((dbType || '').trim().toLowerCase())) {
+ if (['rocketmq', 'mqtt', 'kafka', 'rabbitmq'].includes((dbType || '').trim().toLowerCase())) {
return quoteIdentPart(dbType, raw);
}
const parts = splitQualifiedNameSegments(raw).filter(Boolean);
diff --git a/frontend/src/utils/sqlDialect.test.ts b/frontend/src/utils/sqlDialect.test.ts
index 33a5f2d..8efd18c 100644
--- a/frontend/src/utils/sqlDialect.test.ts
+++ b/frontend/src/utils/sqlDialect.test.ts
@@ -38,6 +38,8 @@ describe('sqlDialect', () => {
expect(resolveSqlDialect('custom', 'qdrant-db')).toBe('qdrant');
expect(resolveSqlDialect('Apache-IoTDB')).toBe('iotdb');
expect(resolveSqlDialect('custom', 'apache_iotdb')).toBe('iotdb');
+ expect(resolveSqlDialect('Rocket-MQ')).toBe('rocketmq');
+ expect(resolveSqlDialect('custom', 'rmq')).toBe('rocketmq');
expect(resolveSqlDialect('MQTTS')).toBe('mqtt');
expect(resolveSqlDialect('custom', 'mqtts')).toBe('mqtt');
expect(resolveSqlDialect('Apache-Kafka')).toBe('kafka');
@@ -77,6 +79,11 @@ describe('sqlDialect', () => {
expect(resolveSqlKeywords('iotdb')).not.toEqual(expect.arrayContaining(['TAGS', 'USING']));
});
+ it('resolves RocketMQ completion keywords for topic discovery and consume syntax', () => {
+ expect(resolveSqlKeywords('rocketmq')).toEqual(expect.arrayContaining(['SHOW TOPICS', 'DESCRIBE TOPIC', 'CONSUME']));
+ expect(resolveSqlKeywords('rocketmq')).not.toEqual(expect.arrayContaining(['ALIGN BY DEVICE', 'AUTO_INCREMENT']));
+ });
+
it('resolves MQTT completion keywords for topic discovery and consume syntax', () => {
expect(resolveSqlKeywords('mqtt')).toEqual(expect.arrayContaining(['SHOW TOPICS', 'DESCRIBE TOPIC', 'CONSUME']));
expect(resolveSqlKeywords('mqtt')).not.toEqual(expect.arrayContaining(['ALIGN BY DEVICE', 'AUTO_INCREMENT']));
diff --git a/frontend/src/utils/sqlDialect.ts b/frontend/src/utils/sqlDialect.ts
index e123bfb..6654046 100644
--- a/frontend/src/utils/sqlDialect.ts
+++ b/frontend/src/utils/sqlDialect.ts
@@ -29,6 +29,7 @@ export type SqlDialect =
| 'clickhouse'
| 'tdengine'
| 'iotdb'
+ | 'rocketmq'
| 'mqtt'
| 'kafka'
| 'rabbitmq'
@@ -138,6 +139,13 @@ export const resolveSqlDialect = (
case 'apache-iotdb':
case 'apache_iotdb':
return 'iotdb';
+ case 'rocketmq':
+ case 'rocket-mq':
+ case 'rocket_mq':
+ case 'apache-rocketmq':
+ case 'apache_rocketmq':
+ case 'rmq':
+ return 'rocketmq';
case 'mqtt':
case 'mqtts':
return 'mqtt';
@@ -173,6 +181,7 @@ export const resolveSqlDialect = (
if (source.includes('clickhouse')) return 'clickhouse';
if (source.includes('tdengine')) return 'tdengine';
if (source.includes('iotdb')) return 'iotdb';
+ if (source.includes('rocketmq') || source.includes('rocket-mq') || source.includes('rocket_mq') || source === 'rmq') return 'rocketmq';
if (source.includes('mqtt')) return 'mqtt';
if (source.includes('kafka')) return 'kafka';
if (source.includes('rabbitmq') || source.includes('rabbit-mq') || source.includes('rabbit_mq')) return 'rabbitmq';
@@ -628,6 +637,15 @@ const IOTDB_KEYWORDS = [
'COMPRESSION',
];
+const ROCKETMQ_KEYWORDS = [
+ 'SHOW TOPICS',
+ 'DESCRIBE TOPIC',
+ 'CONSUME',
+ 'FROM',
+ 'LIMIT',
+ 'OFFSET',
+];
+
const MQTT_KEYWORDS = [
'SHOW TOPICS',
'DESCRIBE TOPIC',
@@ -672,6 +690,7 @@ export const resolveSqlKeywords = (dbType: string): string[] => {
if (dialect === 'clickhouse') return unique([...COMMON_KEYWORDS, ...CLICKHOUSE_KEYWORDS]);
if (dialect === 'tdengine') return unique([...COMMON_KEYWORDS, ...TDENGINE_KEYWORDS]);
if (dialect === 'iotdb') return unique([...COMMON_KEYWORDS, ...IOTDB_KEYWORDS]);
+ if (dialect === 'rocketmq') return unique([...COMMON_KEYWORDS, ...ROCKETMQ_KEYWORDS]);
if (dialect === 'mqtt') return unique([...COMMON_KEYWORDS, ...MQTT_KEYWORDS]);
if (dialect === 'kafka') return unique([...COMMON_KEYWORDS, ...KAFKA_KEYWORDS]);
if (dialect === 'rabbitmq') return unique([...COMMON_KEYWORDS, ...RABBITMQ_KEYWORDS]);
diff --git a/go.mod b/go.mod
index 3a06bac..6240e3a 100644
--- a/go.mod
+++ b/go.mod
@@ -10,8 +10,8 @@ require (
github.com/apache/iotdb-client-go v1.3.7
github.com/caretdev/go-irisnative v0.2.1
github.com/duckdb/duckdb-go/v2 v2.5.5
- github.com/elastic/go-elasticsearch/v8 v8.19.6
github.com/eclipse/paho.mqtt.golang v1.5.1
+ github.com/elastic/go-elasticsearch/v8 v8.19.6
github.com/go-sql-driver/mysql v1.9.3
github.com/google/uuid v1.6.0
github.com/highgo/pq-sm3 v0.0.0
@@ -35,20 +35,32 @@ require (
)
require (
+ github.com/apache/rocketmq-client-go/v2 v2.1.2 // indirect
github.com/apache/thrift v0.22.0 // indirect
github.com/elastic/elastic-transport-go/v8 v8.9.0 // indirect
+ github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/golang/mock v1.5.0 // indirect
github.com/google/jsonschema-go v0.4.3 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
+ github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/segmentio/encoding v0.5.4 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/tidwall/gjson v1.14.2 // indirect
+ github.com/tidwall/match v1.1.1 // indirect
+ github.com/tidwall/pretty v1.2.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
+ go.uber.org/atomic v1.5.1 // indirect
+ golang.org/x/lint v0.0.0-20190930215403-16217165b5de // indirect
golang.org/x/oauth2 v0.35.0 // indirect
+ gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
+ stathat.com/c/consistent v1.0.0 // indirect
)
require (
diff --git a/go.sum b/go.sum
index 78d9524..235c3d6 100644
--- a/go.sum
+++ b/go.sum
@@ -22,6 +22,7 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM=
github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw=
github.com/ClickHouse/clickhouse-go/v2 v2.43.0 h1:fUR05TrF1GyvLDa/mAQjkx7KbgwdLRffs2n9O3WobtE=
@@ -34,6 +35,8 @@ github.com/apache/arrow-go/v18 v18.5.1 h1:yaQ6zxMGgf9YCYw4/oaeOU3AULySDlAYDOcnr4
github.com/apache/arrow-go/v18 v18.5.1/go.mod h1:OCCJsmdq8AsRm8FkBSSmYTwL/s4zHW9CqxeBxEytkNE=
github.com/apache/iotdb-client-go v1.3.7 h1:NHEW0yysGfxFQkkJpFHTlww1a/RHCINbOXBfv2/aIQ0=
github.com/apache/iotdb-client-go v1.3.7/go.mod h1:3D6QYkqRmASS/4HsjU+U/3fscyc5M9xKRfywZsKuoZY=
+github.com/apache/rocketmq-client-go/v2 v2.1.2 h1:yt73olKe5N6894Dbm+ojRf/JPiP0cxfDNNffKwhpJVg=
+github.com/apache/rocketmq-client-go/v2 v2.1.2/go.mod h1:6I6vgxHR3hzrvn+6n/4mrhS+UTulzK/X9LB2Vk1U5gE=
github.com/apache/thrift v0.15.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc=
github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g=
@@ -44,8 +47,12 @@ github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdb
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
@@ -80,9 +87,14 @@ github.com/elastic/elastic-transport-go/v8 v8.9.0 h1:KeT/2P54F0xS0S8Y3Pf+tFDg4Hm
github.com/elastic/elastic-transport-go/v8 v8.9.0/go.mod h1:ssMTvNS2hwf7CaiGsRRsx4gQHFZ/jS/DkLcISxekWzc=
github.com/elastic/go-elasticsearch/v8 v8.19.6 h1:4qa7ecJkr5rLsoHKIVGbaqcFt2o57CnOHQJi9Pts/rk=
github.com/elastic/go-elasticsearch/v8 v8.19.6/go.mod h1:jeWebApE1oFEW/hKZqx/IRYmP/aa2+WMJkOfk+AduSI=
+github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
+github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
+github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
@@ -94,8 +106,10 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
+github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
@@ -113,6 +127,8 @@ github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -124,6 +140,7 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -140,10 +157,13 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@@ -154,6 +174,8 @@ github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bP
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -164,6 +186,7 @@ github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4P
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4=
@@ -173,7 +196,9 @@ github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -227,6 +252,19 @@ github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ib
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
+github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
+github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
+github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
+github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
+github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s=
github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
@@ -234,6 +272,7 @@ github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -266,12 +305,20 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sijms/go-ora/v2 v2.9.0 h1:+iQbUeTeCOFMb5BsOMgUhV8KWyrv9yjKpcK4x7+MFrg=
github.com/sijms/go-ora/v2 v2.9.0/go.mod h1:QgFInVi3ZWyqAiJwzBQA+nbKYKH77tdp1PYoCqhR2dU=
+github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -281,7 +328,14 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/taosdata/driver-go/v3 v3.7.8 h1:N2H6HLLZH2ve2ipcoFgG9BJS+yW0XksqNYwEdSmHaJk=
github.com/taosdata/driver-go/v3 v3.7.8/go.mod h1:gSxBEPOueMg0rTmMO1Ug6aeD7AwGdDGvUtLrsDTTpYc=
+github.com/tidwall/gjson v1.13.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo=
+github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4=
github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
@@ -341,8 +395,11 @@ go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKz
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
+go.uber.org/atomic v1.5.1 h1:rsqfU5vBkVknbhUGbAUwQKR2H4ItV8tjJ+6kJX4cxHM=
+go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -359,6 +416,8 @@ golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -366,14 +425,17 @@ golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -392,15 +454,25 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -425,9 +497,13 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
@@ -455,12 +531,21 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -494,3 +579,5 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+stathat.com/c/consistent v1.0.0 h1:ezyc51EGcRPJUxfHGSgJjWzJdj3NiMU9pNfLNGiXV0c=
+stathat.com/c/consistent v1.0.0/go.mod h1:QkzMWzcbB+yQBL2AttO6sgsQS/JSTapcDISJalmCDS0=
diff --git a/internal/app/db_context.go b/internal/app/db_context.go
index f57307f..3930394 100644
--- a/internal/app/db_context.go
+++ b/internal/app/db_context.go
@@ -16,6 +16,8 @@ func normalizeRunConfig(config connection.ConnectionConfig, dbName string) conne
}
switch strings.ToLower(strings.TrimSpace(config.Type)) {
+ case "rocketmq", "rocket-mq", "rocket_mq", "apache-rocketmq", "apache_rocketmq", "rmq":
+ // RocketMQ 的 Database 字段表示默认 Topic,不能被树上的 synthetic database(topics) 覆盖。
case "mqtt", "mqtts":
// MQTT 的 Database 字段表示默认 Topic,不能被树上的 synthetic database(topics) 覆盖。
case "kafka", "apache-kafka", "apache_kafka":
@@ -55,7 +57,7 @@ func normalizeSchemaAndTable(config connection.ConnectionConfig, dbName string,
// Elasticsearch:索引名可能含多个点(如 iot_pro_biz_operate_log.index.20240626),
// 不能按点分割,直接返回原始数据库名和完整表名。
- if dbType == "elasticsearch" || dbType == "iotdb" || dbType == "mqtt" || dbType == "kafka" || dbType == "rabbitmq" {
+ if dbType == "elasticsearch" || dbType == "iotdb" || dbType == "rocketmq" || dbType == "mqtt" || dbType == "kafka" || dbType == "rabbitmq" {
return rawDB, rawTable
}
@@ -114,7 +116,7 @@ func normalizeSchemaAndTable(config connection.ConnectionConfig, dbName string,
func normalizeMetadataSchemaAndTable(config connection.ConnectionConfig, dbName string, tableName string) (string, string) {
schema, table := normalizeSchemaAndTable(config, dbName, tableName)
switch resolveDDLDBType(config) {
- case "mqtt", "kafka", "rabbitmq":
+ case "rocketmq", "mqtt", "kafka", "rabbitmq":
return schema, table
case "postgres", "kingbase", "highgo", "vastbase", "opengauss", "gaussdb":
rawTable := strings.TrimSpace(tableName)
diff --git a/internal/app/db_context_test.go b/internal/app/db_context_test.go
index 2aaa292..31a11cd 100644
--- a/internal/app/db_context_test.go
+++ b/internal/app/db_context_test.go
@@ -344,6 +344,18 @@ func TestNormalizeSchemaAndTable_MQTTPreservesTopicFilter(t *testing.T) {
}
}
+func TestNormalizeSchemaAndTable_RocketMQPreservesTopicName(t *testing.T) {
+ t.Parallel()
+
+ schemaOrDb, table := normalizeSchemaAndTable(connection.ConnectionConfig{
+ Type: "rocketmq",
+ }, "topics", "orders.events.v1")
+
+ if schemaOrDb != "topics" || table != "orders.events.v1" {
+ t.Fatalf("expected rocketmq topic name to stay intact, got %q.%q", schemaOrDb, table)
+ }
+}
+
func TestNormalizeSchemaAndTable_RabbitMQPreservesDottedQueueName(t *testing.T) {
t.Parallel()
@@ -380,6 +392,18 @@ func TestNormalizeMetadataSchemaAndTable_MQTTPreservesTopicFilter(t *testing.T)
}
}
+func TestNormalizeMetadataSchemaAndTable_RocketMQPreservesTopicName(t *testing.T) {
+ t.Parallel()
+
+ schemaOrDb, table := normalizeMetadataSchemaAndTable(connection.ConnectionConfig{
+ Type: "rocketmq",
+ }, "topics", "orders.events.v1")
+
+ if schemaOrDb != "topics" || table != "orders.events.v1" {
+ t.Fatalf("expected rocketmq metadata topic name to stay intact, got %q.%q", schemaOrDb, table)
+ }
+}
+
func TestNormalizeMetadataSchemaAndTable_RabbitMQPreservesDottedQueueName(t *testing.T) {
t.Parallel()
diff --git a/internal/app/methods_db.go b/internal/app/methods_db.go
index cf2fd66..8a47f6b 100644
--- a/internal/app/methods_db.go
+++ b/internal/app/methods_db.go
@@ -315,8 +315,8 @@ func normalizeSchemaAndTableByType(dbType string, dbName string, tableName strin
return rawDB, rawTable
}
- // Elasticsearch / MQTT / RabbitMQ / Kafka:对象名可能含多个点或路径,不能按点分割
- if dbType == "elasticsearch" || dbType == "mqtt" || dbType == "kafka" || dbType == "rabbitmq" {
+ // Elasticsearch / RocketMQ / MQTT / RabbitMQ / Kafka:对象名可能含多个点或路径,不能按点分割
+ if dbType == "elasticsearch" || dbType == "rocketmq" || dbType == "mqtt" || dbType == "kafka" || dbType == "rabbitmq" {
return rawDB, rawTable
}
diff --git a/internal/app/methods_db_create_statement_test.go b/internal/app/methods_db_create_statement_test.go
index 4c9599c..9faac18 100644
--- a/internal/app/methods_db_create_statement_test.go
+++ b/internal/app/methods_db_create_statement_test.go
@@ -175,6 +175,15 @@ func TestNormalizeSchemaAndTableByType_MQTTPreservesTopicFilter(t *testing.T) {
}
}
+func TestNormalizeSchemaAndTableByType_RocketMQPreservesTopicName(t *testing.T) {
+ t.Parallel()
+
+ schema, table := normalizeSchemaAndTableByType("rocketmq", "topics", "orders.events.v1")
+ if schema != "topics" || table != "orders.events.v1" {
+ t.Fatalf("expected rocketmq topic name to stay intact, got %q.%q", schema, table)
+ }
+}
+
func TestNormalizeSchemaAndTableByType_RabbitMQPreservesDottedQueueName(t *testing.T) {
t.Parallel()
diff --git a/internal/app/methods_driver.go b/internal/app/methods_driver.go
index 254f549..055c13a 100644
--- a/internal/app/methods_driver.go
+++ b/internal/app/methods_driver.go
@@ -1432,6 +1432,8 @@ func normalizeDriverType(driverType string) string {
return "gaussdb"
case "goldendb", "greatdb", "gdb":
return "goldendb"
+ case "rocketmq", "rocket-mq", "rocket_mq", "apache-rocketmq", "apache_rocketmq", "rmq":
+ return "rocketmq"
case "mqtt", "mqtts":
return "mqtt"
case "kafka", "apache-kafka", "apache_kafka":
@@ -1507,6 +1509,7 @@ func allDriverDefinitionsWithPackages(packages map[string]pinnedDriverPackage) [
{Type: "oracle", Name: "Oracle", Engine: driverEngineGo, BuiltIn: true},
{Type: "redis", Name: "Redis", Engine: driverEngineGo, BuiltIn: true},
{Type: "postgres", Name: "PostgreSQL", Engine: driverEngineGo, BuiltIn: true},
+ {Type: "rocketmq", Name: "RocketMQ", Engine: driverEngineGo, BuiltIn: true},
{Type: "mqtt", Name: "MQTT", Engine: driverEngineGo, BuiltIn: true},
{Type: "kafka", Name: "Kafka", Engine: driverEngineGo, BuiltIn: true},
{Type: "rabbitmq", Name: "RabbitMQ", Engine: driverEngineGo, BuiltIn: true},
diff --git a/internal/app/methods_driver_version_test.go b/internal/app/methods_driver_version_test.go
index 8c73e62..7841782 100644
--- a/internal/app/methods_driver_version_test.go
+++ b/internal/app/methods_driver_version_test.go
@@ -534,6 +534,22 @@ func TestMQTTDriverDefinitionIsBuiltIn(t *testing.T) {
}
}
+func TestRocketMQDriverDefinitionIsBuiltIn(t *testing.T) {
+ definition, ok := resolveDriverDefinition("rmq")
+ if !ok {
+ t.Fatal("expected rocketmq driver definition")
+ }
+ if definition.Name != "RocketMQ" {
+ t.Fatalf("unexpected rocketmq driver name: %q", definition.Name)
+ }
+ if !definition.BuiltIn {
+ t.Fatal("expected rocketmq to be a built-in driver")
+ }
+ if definition.PinnedVersion != "" || definition.DefaultDownloadURL != "" {
+ t.Fatalf("expected rocketmq builtin definition to omit optional-agent metadata: %#v", definition)
+ }
+}
+
func TestRabbitMQDriverDefinitionIsBuiltIn(t *testing.T) {
definition, ok := resolveDriverDefinition("rabbit-mq")
if !ok {
diff --git a/internal/db/database.go b/internal/db/database.go
index a5aacda..513fda9 100644
--- a/internal/db/database.go
+++ b/internal/db/database.go
@@ -489,6 +489,9 @@ var databaseFactories = map[string]databaseFactory{
"qdrant": func() Database {
return &QdrantDB{}
},
+ "rocketmq": func() Database {
+ return &RocketMQDB{}
+ },
"mqtt": func() Database {
return &MQTTDB{}
},
@@ -538,6 +541,8 @@ func normalizeDatabaseType(dbType string) string {
return "chroma"
case "qdrantdb", "qdrant-db":
return "qdrant"
+ case "rocketmq", "rocket-mq", "rocket_mq", "apache-rocketmq", "apache_rocketmq", "rmq":
+ return "rocketmq"
case "mqtt", "mqtts":
return "mqtt"
case "kafka", "apache-kafka", "apache_kafka":
diff --git a/internal/db/driver_support.go b/internal/db/driver_support.go
index cb90897..664fd19 100644
--- a/internal/db/driver_support.go
+++ b/internal/db/driver_support.go
@@ -19,6 +19,7 @@ var coreBuiltinDrivers = map[string]struct{}{
"postgres": {},
"chroma": {},
"qdrant": {},
+ "rocketmq": {},
"mqtt": {},
"kafka": {},
"rabbitmq": {},
@@ -82,6 +83,8 @@ func normalizeRuntimeDriverType(driverType string) string {
return "chroma"
case "qdrantdb", "qdrant-db":
return "qdrant"
+ case "rocketmq", "rocket-mq", "rocket_mq", "apache-rocketmq", "apache_rocketmq", "rmq":
+ return "rocketmq"
case "mqtt", "mqtts":
return "mqtt"
case "apache-iotdb", "apache_iotdb", "iotdb":
@@ -151,6 +154,8 @@ func driverDisplayName(driverType string) string {
return "Chroma"
case "qdrant":
return "Qdrant"
+ case "rocketmq":
+ return "RocketMQ"
case "mqtt":
return "MQTT"
case "kafka":
diff --git a/internal/db/rocketmq_impl.go b/internal/db/rocketmq_impl.go
new file mode 100644
index 0000000..b091e57
--- /dev/null
+++ b/internal/db/rocketmq_impl.go
@@ -0,0 +1,1482 @@
+package db
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/url"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "GoNavi-Wails/internal/connection"
+
+ rocketmq "github.com/apache/rocketmq-client-go/v2"
+ rocketmqadmin "github.com/apache/rocketmq-client-go/v2/admin"
+ rocketmqconsumer "github.com/apache/rocketmq-client-go/v2/consumer"
+ rocketmqprimitive "github.com/apache/rocketmq-client-go/v2/primitive"
+ rocketmqproducer "github.com/apache/rocketmq-client-go/v2/producer"
+)
+
+const (
+ defaultRocketMQPort = 9876
+ defaultRocketMQQueryTimeout = 30 * time.Second
+ defaultRocketMQPreviewLimit = 100
+ defaultRocketMQPullBatchSize = 32
+ maxRocketMQPullBatchSize = 256
+ rocketMQSyntheticDatabase = "topics"
+ rocketMQDefaultProducerGroup = "GoNaviRocketMQProducer"
+ rocketMQDefaultConsumerGroup = "GoNaviRocketMQPreview"
+ rocketMQDefaultInstancePrefix = "GoNavi"
+)
+
+type rocketmqRuntime interface {
+ Close() error
+ Ping(ctx context.Context) error
+ ListTopics(ctx context.Context, includeSystem bool) ([]rocketmqTopicInfo, error)
+ DescribeTopic(ctx context.Context, request rocketmqDescribeRequest) (rocketmqTopicDescription, error)
+ FetchMessages(ctx context.Context, request rocketmqFetchRequest) ([]rocketmqMessageRecord, error)
+ Publish(ctx context.Context, command rocketmqPublishCommand) (int64, error)
+}
+
+type rocketmqDescribeRequest struct {
+ Topic string
+ ConsumerGroup string
+ TagExpression string
+ PullBatchSize int
+}
+
+type rocketmqTopicInfo struct {
+ Name string
+ System bool
+ QueueCount int
+}
+
+type rocketmqTopicDescription struct {
+ Name string
+ Namespace string
+ ConsumerGroup string
+ TagExpression string
+ QueueCount int
+ TotalApproximateCount int64
+ Queues []rocketmqTopicQueueInfo
+}
+
+type rocketmqTopicQueueInfo struct {
+ BrokerName string
+ QueueID int
+ MinOffset int64
+ MaxOffset int64
+ ApproximateCount int64
+}
+
+type rocketmqFetchRequest struct {
+ Topic string
+ Limit int
+ Offset int
+ ConsumerGroup string
+ TagExpression string
+ Latest bool
+ PullBatchSize int
+}
+
+type rocketmqPublishCommand struct {
+ Topic string
+ Payload interface{}
+ Tag string
+ Keys []string
+ DelayLevel int
+ Properties map[string]string
+}
+
+type rocketmqMessageRecord struct {
+ Topic string
+ BrokerName string
+ QueueID int
+ QueueOffset int64
+ MsgID string
+ OffsetMsgID string
+ Tags string
+ Keys string
+ Body []byte
+ Decoded interface{}
+ Encoding string
+ Properties map[string]string
+ BornTimestamp time.Time
+ StoreTimestamp time.Time
+ ReconsumeTimes int32
+ MinOffset int64
+ MaxOffset int64
+}
+
+type nativeRocketMQRuntime struct {
+ config connection.ConnectionConfig
+ nameservers []string
+ namespace string
+ timeout time.Duration
+ sendTimeout time.Duration
+}
+
+var newRocketMQRuntime = func(config connection.ConnectionConfig) (rocketmqRuntime, error) {
+ return newNativeRocketMQRuntime(config)
+}
+
+type RocketMQDB struct {
+ runtime rocketmqRuntime
+ defaultTopic string
+ defaultConsumerGroup string
+ defaultTagExpression string
+ startLatest bool
+ pullBatchSize int
+ namespace string
+}
+
+func (r *RocketMQDB) Connect(config connection.ConnectionConfig) error {
+ _ = r.Close()
+
+ runConfig := normalizeRocketMQConfig(config)
+ if runConfig.UseSSH {
+ return fmt.Errorf("RocketMQ 当前暂不支持 SSH 隧道;请直接连通 NameServer 与 Broker")
+ }
+ if runConfig.UseProxy || runConfig.UseHTTPTunnel {
+ return fmt.Errorf("RocketMQ 当前暂不支持代理或 HTTP 隧道;请直接连通 NameServer 与 Broker")
+ }
+
+ runtime, err := newRocketMQRuntime(runConfig)
+ if err != nil {
+ return err
+ }
+ r.runtime = runtime
+ r.defaultTopic = rocketmqDefaultTopic(runConfig)
+ r.defaultConsumerGroup = rocketmqConfiguredConsumerGroup(runConfig)
+ r.defaultTagExpression = rocketmqConfiguredTagExpression(runConfig)
+ r.startLatest = rocketmqDefaultStartLatest(runConfig)
+ r.pullBatchSize = rocketmqPullBatchSize(runConfig)
+ r.namespace = rocketmqNamespace(runConfig)
+
+ if err := r.Ping(); err != nil {
+ _ = r.Close()
+ return err
+ }
+ return nil
+}
+
+func (r *RocketMQDB) Close() error {
+ var firstErr error
+ if r.runtime != nil {
+ if err := r.runtime.Close(); err != nil && firstErr == nil {
+ firstErr = err
+ }
+ }
+ r.runtime = nil
+ r.defaultTopic = ""
+ r.defaultConsumerGroup = ""
+ r.defaultTagExpression = ""
+ r.startLatest = false
+ r.pullBatchSize = 0
+ r.namespace = ""
+ return firstErr
+}
+
+func (r *RocketMQDB) Ping() error {
+ if r.runtime == nil {
+ return fmt.Errorf("连接未打开")
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ return r.runtime.Ping(ctx)
+}
+
+func (r *RocketMQDB) Query(query string) ([]map[string]interface{}, []string, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), defaultRocketMQQueryTimeout)
+ defer cancel()
+ return r.QueryContext(ctx, query)
+}
+
+func (r *RocketMQDB) QueryContext(ctx context.Context, query string) ([]map[string]interface{}, []string, error) {
+ if r.runtime == nil {
+ return nil, nil, fmt.Errorf("连接未打开")
+ }
+ text := strings.TrimSpace(query)
+ if text == "" {
+ return nil, nil, fmt.Errorf("查询语句不能为空")
+ }
+
+ parsed, ok := parseRocketMQSQL(text, r.startLatest)
+ if !ok {
+ return nil, nil, fmt.Errorf("RocketMQ 查询仅支持 SHOW TOPICS、DESCRIBE TOPIC、SELECT * FROM topic 与 CONSUME FROM topic")
+ }
+
+ switch parsed.Action {
+ case "show_topics":
+ topics, err := r.runtime.ListTopics(ctx, false)
+ if err != nil {
+ return nil, nil, err
+ }
+ rows := rocketmqTopicRows(topics)
+ if parsed.Limit > 0 && len(rows) > parsed.Limit {
+ rows = rows[:parsed.Limit]
+ }
+ return rows, collectColumns(rows), nil
+ case "describe_topic":
+ topic := rocketmqResolveTopic(parsed.Topic, r.defaultTopic)
+ if topic == "" {
+ return nil, nil, fmt.Errorf("RocketMQ topic 不能为空")
+ }
+ description, err := r.runtime.DescribeTopic(ctx, rocketmqDescribeRequest{
+ Topic: topic,
+ ConsumerGroup: r.resolveConsumerGroup("describe"),
+ TagExpression: r.defaultTagExpression,
+ PullBatchSize: r.pullBatchSize,
+ })
+ if err != nil {
+ return nil, nil, err
+ }
+ rows := rocketmqDescribeRows(description)
+ return rows, collectColumns(rows), nil
+ case "select", "consume":
+ topic := rocketmqResolveTopic(parsed.Topic, r.defaultTopic)
+ if topic == "" {
+ return nil, nil, fmt.Errorf("RocketMQ topic 不能为空")
+ }
+ if parsed.Count {
+ if !rocketmqTagExpressionIsDefault(r.defaultTagExpression) {
+ return nil, nil, fmt.Errorf("RocketMQ 配置了 TAG 过滤时暂不支持 COUNT(*) 总量统计;请改为手动预览消息")
+ }
+ description, err := r.runtime.DescribeTopic(ctx, rocketmqDescribeRequest{
+ Topic: topic,
+ ConsumerGroup: r.resolveConsumerGroup("count"),
+ TagExpression: r.defaultTagExpression,
+ PullBatchSize: r.pullBatchSize,
+ })
+ if err != nil {
+ return nil, nil, err
+ }
+ rows := []map[string]interface{}{{
+ "topic": topic,
+ "queue_count": description.QueueCount,
+ "total_approximate_count": description.TotalApproximateCount,
+ "namespace": description.Namespace,
+ }}
+ return rows, []string{"topic", "queue_count", "total_approximate_count", "namespace"}, nil
+ }
+ records, err := r.runtime.FetchMessages(ctx, rocketmqFetchRequest{
+ Topic: topic,
+ Limit: parsed.Limit,
+ Offset: parsed.Offset,
+ ConsumerGroup: r.resolveConsumerGroup(parsed.Action),
+ TagExpression: r.defaultTagExpression,
+ Latest: parsed.Latest,
+ PullBatchSize: r.pullBatchSize,
+ })
+ if err != nil {
+ return nil, nil, err
+ }
+ rows := rocketmqMessageRows(records)
+ return rows, collectColumns(rows), nil
+ default:
+ return nil, nil, fmt.Errorf("未实现的 RocketMQ 查询类型:%s", parsed.Action)
+ }
+}
+
+func (r *RocketMQDB) Exec(query string) (int64, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), defaultRocketMQQueryTimeout)
+ defer cancel()
+ return r.ExecContext(ctx, query)
+}
+
+func (r *RocketMQDB) ExecContext(ctx context.Context, query string) (int64, error) {
+ if r.runtime == nil {
+ return 0, fmt.Errorf("连接未打开")
+ }
+ var cmd map[string]interface{}
+ if err := decodeJSONWithUseNumber([]byte(strings.TrimSpace(query)), &cmd); err != nil {
+ return 0, fmt.Errorf("RocketMQ 写入命令必须是 JSON:%w", err)
+ }
+ topic := rocketmqResolveTopic(firstStringValue(cmd, "publish", "topic", "destination"), r.defaultTopic)
+ if topic == "" {
+ return 0, fmt.Errorf("RocketMQ publish 命令缺少 topic")
+ }
+ if !hasAnyKey(cmd, "payload", "value", "body", "message") {
+ return 0, fmt.Errorf("RocketMQ publish 命令缺少 payload")
+ }
+ keys, err := rocketmqKeysFromAny(firstExisting(cmd, "keys", "key", "messageKeys", "message_keys"))
+ if err != nil {
+ return 0, err
+ }
+ properties, err := rocketmqPropertiesFromAny(firstExisting(cmd, "properties", "userProperties", "user_properties"))
+ if err != nil {
+ return 0, err
+ }
+ delayLevel, err := rocketmqDelayLevelFromAny(firstExisting(cmd, "delayLevel", "delay_level", "delay"))
+ if err != nil {
+ return 0, err
+ }
+ return r.runtime.Publish(ctx, rocketmqPublishCommand{
+ Topic: topic,
+ Payload: firstExisting(cmd, "payload", "value", "body", "message"),
+ Tag: strings.TrimSpace(firstStringValue(cmd, "tag", "tags")),
+ Keys: keys,
+ DelayLevel: delayLevel,
+ Properties: properties,
+ })
+}
+
+func (r *RocketMQDB) GetDatabases() ([]string, error) {
+ if r.runtime == nil {
+ return nil, fmt.Errorf("连接未打开")
+ }
+ return []string{rocketMQSyntheticDatabase}, nil
+}
+
+func (r *RocketMQDB) GetTables(dbName string) ([]string, error) {
+ if r.runtime == nil {
+ return nil, fmt.Errorf("连接未打开")
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ topics, err := r.runtime.ListTopics(ctx, false)
+ if err != nil {
+ return nil, err
+ }
+ names := make([]string, 0, len(topics))
+ for _, topic := range topics {
+ if strings.TrimSpace(topic.Name) != "" {
+ names = append(names, topic.Name)
+ }
+ }
+ sort.Strings(names)
+ return names, nil
+}
+
+func (r *RocketMQDB) GetCreateStatement(dbName, tableName string) (string, error) {
+ if r.runtime == nil {
+ return "", fmt.Errorf("连接未打开")
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ topic := rocketmqResolveTopic(tableName, r.defaultTopic)
+ if topic == "" {
+ return "", fmt.Errorf("RocketMQ topic 不能为空")
+ }
+ description, err := r.runtime.DescribeTopic(ctx, rocketmqDescribeRequest{
+ Topic: topic,
+ ConsumerGroup: r.resolveConsumerGroup("ddl"),
+ TagExpression: r.defaultTagExpression,
+ PullBatchSize: r.pullBatchSize,
+ })
+ if err != nil {
+ return "", err
+ }
+ payload, _ := json.MarshalIndent(description, "", " ")
+ return fmt.Sprintf("// RocketMQ topic: %s\n%s", topic, string(payload)), nil
+}
+
+func (r *RocketMQDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefinition, error) {
+ if r.runtime == nil {
+ return nil, fmt.Errorf("连接未打开")
+ }
+ topic := rocketmqResolveTopic(tableName, r.defaultTopic)
+ if topic == "" {
+ return nil, fmt.Errorf("RocketMQ topic 不能为空")
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ records, err := r.runtime.FetchMessages(ctx, rocketmqFetchRequest{
+ Topic: topic,
+ Limit: 20,
+ ConsumerGroup: r.resolveConsumerGroup("columns"),
+ TagExpression: r.defaultTagExpression,
+ Latest: false,
+ PullBatchSize: r.pullBatchSize,
+ })
+ if err != nil {
+ return nil, err
+ }
+ rows := rocketmqMessageRows(records)
+ columns := []connection.ColumnDefinition{
+ {Name: "topic", Type: "string", Nullable: "NO", Comment: "RocketMQ topic"},
+ {Name: "broker_name", Type: "string", Nullable: "NO", Comment: "Broker name"},
+ {Name: "queue_id", Type: "int", Nullable: "NO", Key: "PRI", Comment: "Queue id"},
+ {Name: "queue_offset", Type: "bigint", Nullable: "NO", Key: "PRI", Comment: "Queue offset"},
+ {Name: "msg_id", Type: "string", Nullable: "YES", Comment: "Message id"},
+ {Name: "offset_msg_id", Type: "string", Nullable: "YES", Comment: "Offset message id"},
+ {Name: "tags", Type: "string", Nullable: "YES", Comment: "RocketMQ tag"},
+ {Name: "keys", Type: "string", Nullable: "YES", Comment: "RocketMQ keys"},
+ {Name: "born_timestamp", Type: "timestamp", Nullable: "YES", Comment: "Born timestamp"},
+ {Name: "store_timestamp", Type: "timestamp", Nullable: "YES", Comment: "Store timestamp"},
+ {Name: "reconsume_times", Type: "int", Nullable: "YES", Comment: "Reconsume times"},
+ {Name: "body", Type: "json", Nullable: "YES", Comment: "Decoded message body"},
+ {Name: "body_encoding", Type: "string", Nullable: "YES", Comment: "Message body encoding"},
+ {Name: "properties", Type: "json", Nullable: "YES", Comment: "Message properties"},
+ }
+ seen := map[string]struct{}{
+ "topic": {}, "broker_name": {}, "queue_id": {}, "queue_offset": {}, "msg_id": {}, "offset_msg_id": {},
+ "tags": {}, "keys": {}, "born_timestamp": {}, "store_timestamp": {}, "reconsume_times": {},
+ "body": {}, "body_encoding": {}, "properties": {},
+ }
+ for _, row := range rows {
+ for key, value := range row {
+ if _, exists := seen[key]; exists {
+ continue
+ }
+ if !strings.HasPrefix(key, "body.") && !strings.HasPrefix(key, "properties.") {
+ continue
+ }
+ seen[key] = struct{}{}
+ columns = append(columns, connection.ColumnDefinition{
+ Name: key,
+ Type: inferChromaValueType(value),
+ Nullable: "YES",
+ Comment: "Derived RocketMQ field",
+ })
+ }
+ }
+ return columns, nil
+}
+
+func (r *RocketMQDB) GetAllColumns(dbName string) ([]connection.ColumnDefinitionWithTable, error) {
+ tables, err := r.GetTables(dbName)
+ if err != nil {
+ return nil, err
+ }
+ var result []connection.ColumnDefinitionWithTable
+ for _, table := range tables {
+ cols, err := r.GetColumns(dbName, table)
+ if err != nil {
+ continue
+ }
+ for _, col := range cols {
+ result = append(result, connection.ColumnDefinitionWithTable{
+ TableName: table,
+ Name: col.Name,
+ Type: col.Type,
+ Comment: col.Comment,
+ })
+ }
+ }
+ return result, nil
+}
+
+func (r *RocketMQDB) GetIndexes(dbName, tableName string) ([]connection.IndexDefinition, error) {
+ return []connection.IndexDefinition{
+ {Name: "PRIMARY", ColumnName: "queue_id", NonUnique: 0, SeqInIndex: 1, IndexType: "QUEUE_OFFSET"},
+ {Name: "PRIMARY", ColumnName: "queue_offset", NonUnique: 0, SeqInIndex: 2, IndexType: "QUEUE_OFFSET"},
+ {Name: "STORE_TIMESTAMP", ColumnName: "store_timestamp", NonUnique: 1, SeqInIndex: 1, IndexType: "BTREE"},
+ }, nil
+}
+
+func (r *RocketMQDB) GetForeignKeys(dbName, tableName string) ([]connection.ForeignKeyDefinition, error) {
+ return []connection.ForeignKeyDefinition{}, nil
+}
+
+func (r *RocketMQDB) GetTriggers(dbName, tableName string) ([]connection.TriggerDefinition, error) {
+ return []connection.TriggerDefinition{}, nil
+}
+
+func (r *RocketMQDB) ApplyChanges(tableName string, changes connection.ChangeSet) error {
+ if len(changes.Inserts) == 0 && len(changes.Updates) == 0 && len(changes.Deletes) == 0 {
+ return nil
+ }
+ return fmt.Errorf("RocketMQ 结果集仅支持只读预览;如需写入请在 SQL 编辑器执行 JSON publish 命令")
+}
+
+func (r *RocketMQDB) resolveConsumerGroup(purpose string) string {
+ group := strings.TrimSpace(r.defaultConsumerGroup)
+ if group != "" {
+ return group
+ }
+ return fmt.Sprintf("%s-%s-%d", rocketMQDefaultConsumerGroup, purpose, time.Now().UnixNano())
+}
+
+func newNativeRocketMQRuntime(config connection.ConnectionConfig) (rocketmqRuntime, error) {
+ nameservers, err := rocketmqNameServerAddresses(config)
+ if err != nil {
+ return nil, err
+ }
+ timeout := getConnectTimeout(config)
+ if timeout <= 0 {
+ timeout = 10 * time.Second
+ }
+ return &nativeRocketMQRuntime{
+ config: config,
+ nameservers: nameservers,
+ namespace: rocketmqNamespace(config),
+ timeout: timeout,
+ sendTimeout: rocketmqSendTimeout(config),
+ }, nil
+}
+
+func (r *nativeRocketMQRuntime) Close() error {
+ return nil
+}
+
+func (r *nativeRocketMQRuntime) Ping(ctx context.Context) error {
+ adminClient, err := r.newAdmin()
+ if err != nil {
+ return err
+ }
+ defer adminClient.Close()
+ _, err = adminClient.FetchAllTopicList(ctx)
+ return err
+}
+
+func (r *nativeRocketMQRuntime) ListTopics(ctx context.Context, includeSystem bool) ([]rocketmqTopicInfo, error) {
+ adminClient, err := r.newAdmin()
+ if err != nil {
+ return nil, err
+ }
+ defer adminClient.Close()
+
+ response, err := adminClient.FetchAllTopicList(ctx)
+ if err != nil {
+ return nil, err
+ }
+ seen := map[string]struct{}{}
+ topics := make([]rocketmqTopicInfo, 0, len(response.TopicList))
+ for _, name := range response.TopicList {
+ topic := strings.TrimSpace(name)
+ if topic == "" {
+ continue
+ }
+ if _, exists := seen[topic]; exists {
+ continue
+ }
+ seen[topic] = struct{}{}
+ system := rocketmqIsSystemTopic(topic)
+ if system && !includeSystem {
+ continue
+ }
+ queues, err := adminClient.FetchPublishMessageQueues(ctx, topic)
+ queueCount := 0
+ if err == nil {
+ queueCount = len(queues)
+ }
+ topics = append(topics, rocketmqTopicInfo{
+ Name: topic,
+ System: system,
+ QueueCount: queueCount,
+ })
+ }
+ sort.Slice(topics, func(i, j int) bool {
+ return topics[i].Name < topics[j].Name
+ })
+ return topics, nil
+}
+
+func (r *nativeRocketMQRuntime) DescribeTopic(ctx context.Context, request rocketmqDescribeRequest) (rocketmqTopicDescription, error) {
+ adminClient, err := r.newAdmin()
+ if err != nil {
+ return rocketmqTopicDescription{}, err
+ }
+ defer adminClient.Close()
+
+ consumerClient, err := r.newPullConsumer(request.Topic, request.ConsumerGroup, request.TagExpression, request.PullBatchSize)
+ if err != nil {
+ return rocketmqTopicDescription{}, err
+ }
+ defer consumerClient.Shutdown()
+
+ return r.describeTopicWithClients(ctx, adminClient, consumerClient, request)
+}
+
+func (r *nativeRocketMQRuntime) FetchMessages(ctx context.Context, request rocketmqFetchRequest) ([]rocketmqMessageRecord, error) {
+ adminClient, err := r.newAdmin()
+ if err != nil {
+ return nil, err
+ }
+ defer adminClient.Close()
+
+ consumerClient, err := r.newPullConsumer(request.Topic, request.ConsumerGroup, request.TagExpression, request.PullBatchSize)
+ if err != nil {
+ return nil, err
+ }
+ defer consumerClient.Shutdown()
+
+ description, err := r.describeTopicWithClients(ctx, adminClient, consumerClient, rocketmqDescribeRequest{
+ Topic: request.Topic,
+ ConsumerGroup: request.ConsumerGroup,
+ TagExpression: request.TagExpression,
+ PullBatchSize: request.PullBatchSize,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ limit := request.Limit
+ if limit <= 0 {
+ limit = defaultRocketMQPreviewLimit
+ }
+ target := limit + maxInt(request.Offset, 0)
+ if target <= 0 {
+ target = defaultRocketMQPreviewLimit
+ }
+ if request.PullBatchSize <= 0 {
+ request.PullBatchSize = defaultRocketMQPullBatchSize
+ }
+ records := make([]rocketmqMessageRecord, 0, target)
+ for _, queue := range description.Queues {
+ metaQueue := &rocketmqprimitive.MessageQueue{
+ Topic: description.Name,
+ BrokerName: queue.BrokerName,
+ QueueId: queue.QueueID,
+ }
+ items, err := r.fetchQueueMessages(ctx, consumerClient, metaQueue, queue, request, target)
+ if err != nil {
+ return nil, err
+ }
+ records = append(records, items...)
+ }
+ rocketmqSortMessages(records, request.Latest)
+ if request.Offset >= len(records) {
+ return []rocketmqMessageRecord{}, nil
+ }
+ records = records[request.Offset:]
+ if len(records) > limit {
+ records = records[:limit]
+ }
+ return records, nil
+}
+
+func (r *nativeRocketMQRuntime) Publish(ctx context.Context, command rocketmqPublishCommand) (int64, error) {
+ topic := strings.TrimSpace(command.Topic)
+ if topic == "" {
+ return 0, fmt.Errorf("RocketMQ publish 命令缺少 topic")
+ }
+ payload, err := mqttEncodePayload(command.Payload)
+ if err != nil {
+ return 0, fmt.Errorf("序列化 RocketMQ payload 失败:%w", err)
+ }
+
+ producerClient, err := r.newProducer()
+ if err != nil {
+ return 0, err
+ }
+ defer producerClient.Shutdown()
+
+ message := rocketmqprimitive.NewMessage(topic, payload)
+ if tag := strings.TrimSpace(command.Tag); tag != "" {
+ message.WithTag(tag)
+ }
+ if len(command.Keys) > 0 {
+ message.WithKeys(command.Keys)
+ }
+ if command.DelayLevel > 0 {
+ message.WithDelayTimeLevel(command.DelayLevel)
+ }
+ if len(command.Properties) > 0 {
+ for key, value := range command.Properties {
+ if strings.TrimSpace(key) == "" {
+ continue
+ }
+ message.WithProperty(strings.TrimSpace(key), value)
+ }
+ }
+
+ result, err := producerClient.SendSync(ctx, message)
+ if err != nil {
+ return 0, err
+ }
+ if result == nil || result.Status != rocketmqprimitive.SendOK {
+ return 0, fmt.Errorf("RocketMQ 发送失败:状态=%v", result)
+ }
+ return 1, nil
+}
+
+func (r *nativeRocketMQRuntime) describeTopicWithClients(ctx context.Context, adminClient rocketmqadmin.Admin, consumerClient rocketmq.PullConsumer, request rocketmqDescribeRequest) (rocketmqTopicDescription, error) {
+ topic := strings.TrimSpace(request.Topic)
+ if topic == "" {
+ return rocketmqTopicDescription{}, fmt.Errorf("RocketMQ topic 不能为空")
+ }
+ queues, err := adminClient.FetchPublishMessageQueues(ctx, topic)
+ if err != nil {
+ return rocketmqTopicDescription{}, err
+ }
+ sort.Slice(queues, func(i, j int) bool {
+ if queues[i].BrokerName == queues[j].BrokerName {
+ return queues[i].QueueId < queues[j].QueueId
+ }
+ if queues[i].QueueId == queues[j].QueueId {
+ return queues[i].BrokerName < queues[j].BrokerName
+ }
+ return queues[i].QueueId < queues[j].QueueId
+ })
+
+ description := rocketmqTopicDescription{
+ Name: topic,
+ Namespace: r.namespace,
+ ConsumerGroup: strings.TrimSpace(request.ConsumerGroup),
+ TagExpression: rocketmqNormalizeTagExpression(request.TagExpression),
+ QueueCount: len(queues),
+ Queues: make([]rocketmqTopicQueueInfo, 0, len(queues)),
+ }
+ for _, queue := range queues {
+ info, err := r.inspectQueue(ctx, consumerClient, queue)
+ if err != nil {
+ return rocketmqTopicDescription{}, err
+ }
+ description.Queues = append(description.Queues, info)
+ description.TotalApproximateCount += info.ApproximateCount
+ }
+ return description, nil
+}
+
+func (r *nativeRocketMQRuntime) inspectQueue(ctx context.Context, consumerClient rocketmq.PullConsumer, queue *rocketmqprimitive.MessageQueue) (rocketmqTopicQueueInfo, error) {
+ result, err := consumerClient.PullFrom(ctx, queue, 0, 1)
+ if err != nil {
+ return rocketmqTopicQueueInfo{}, err
+ }
+ minOffset := result.MinOffset
+ maxOffset := result.MaxOffset
+ if result.Status == rocketmqprimitive.PullOffsetIllegal && result.NextBeginOffset > minOffset {
+ minOffset = result.NextBeginOffset
+ }
+ if maxOffset < minOffset {
+ maxOffset = minOffset
+ }
+ return rocketmqTopicQueueInfo{
+ BrokerName: queue.BrokerName,
+ QueueID: queue.QueueId,
+ MinOffset: minOffset,
+ MaxOffset: maxOffset,
+ ApproximateCount: maxInt64(0, maxOffset-minOffset),
+ }, nil
+}
+
+func (r *nativeRocketMQRuntime) fetchQueueMessages(ctx context.Context, consumerClient rocketmq.PullConsumer, queue *rocketmqprimitive.MessageQueue, meta rocketmqTopicQueueInfo, request rocketmqFetchRequest, target int) ([]rocketmqMessageRecord, error) {
+ if target <= 0 || meta.MaxOffset <= meta.MinOffset {
+ return []rocketmqMessageRecord{}, nil
+ }
+ startOffset := meta.MinOffset
+ if request.Latest {
+ startOffset = maxInt64(meta.MinOffset, meta.MaxOffset-int64(target))
+ }
+ if startOffset >= meta.MaxOffset {
+ return []rocketmqMessageRecord{}, nil
+ }
+
+ records := make([]rocketmqMessageRecord, 0, target)
+ currentOffset := startOffset
+ batchSize := request.PullBatchSize
+ if batchSize <= 0 {
+ batchSize = defaultRocketMQPullBatchSize
+ }
+ if batchSize > maxRocketMQPullBatchSize {
+ batchSize = maxRocketMQPullBatchSize
+ }
+ for len(records) < target && currentOffset < meta.MaxOffset {
+ numbers := batchSize
+ if remaining := target - len(records); remaining < numbers {
+ numbers = remaining
+ }
+ if numbers <= 0 {
+ break
+ }
+ result, err := consumerClient.PullFrom(ctx, queue, currentOffset, numbers)
+ if err != nil {
+ return nil, err
+ }
+ switch result.Status {
+ case rocketmqprimitive.PullFound:
+ for _, message := range result.GetMessageExts() {
+ records = append(records, rocketmqRecordFromExt(message, queue.BrokerName, queue.QueueId, result.MinOffset, result.MaxOffset))
+ }
+ if result.NextBeginOffset <= currentOffset {
+ return records, nil
+ }
+ currentOffset = result.NextBeginOffset
+ case rocketmqprimitive.PullOffsetIllegal:
+ if result.NextBeginOffset <= currentOffset {
+ return records, nil
+ }
+ currentOffset = result.NextBeginOffset
+ case rocketmqprimitive.PullNoNewMsg, rocketmqprimitive.PullNoMsgMatched, rocketmqprimitive.PullBrokerTimeout:
+ return records, nil
+ default:
+ return records, nil
+ }
+ }
+ return records, nil
+}
+
+func (r *nativeRocketMQRuntime) newAdmin() (rocketmqadmin.Admin, error) {
+ options := []rocketmqadmin.AdminOption{
+ rocketmqadmin.WithResolver(rocketmqprimitive.NewPassthroughResolver(append([]string(nil), r.nameservers...))),
+ }
+ if namespace := strings.TrimSpace(r.namespace); namespace != "" {
+ options = append(options, rocketmqadmin.WithNamespace(namespace))
+ }
+ if credentials, ok := rocketmqCredentials(r.config); ok {
+ options = append(options, rocketmqadmin.WithCredentials(credentials))
+ }
+ return rocketmqadmin.NewAdmin(options...)
+}
+
+func (r *nativeRocketMQRuntime) newProducer() (rocketmq.Producer, error) {
+ group := rocketmqProducerGroup(r.config)
+ if group == "" {
+ group = fmt.Sprintf("%s-%d", rocketMQDefaultProducerGroup, time.Now().UnixNano())
+ }
+ options := []rocketmqproducer.Option{
+ rocketmqproducer.WithNsResolver(rocketmqprimitive.NewPassthroughResolver(append([]string(nil), r.nameservers...))),
+ rocketmqproducer.WithGroupName(group),
+ rocketmqproducer.WithInstanceName(fmt.Sprintf("%s-producer-%d", rocketMQDefaultInstancePrefix, time.Now().UnixNano())),
+ rocketmqproducer.WithRetry(0),
+ rocketmqproducer.WithSendMsgTimeout(r.sendTimeout),
+ }
+ if namespace := strings.TrimSpace(r.namespace); namespace != "" {
+ options = append(options, rocketmqproducer.WithNamespace(namespace))
+ }
+ if credentials, ok := rocketmqCredentials(r.config); ok {
+ options = append(options, rocketmqproducer.WithCredentials(credentials))
+ }
+ client, err := rocketmq.NewProducer(options...)
+ if err != nil {
+ return nil, err
+ }
+ if err := client.Start(); err != nil {
+ return nil, err
+ }
+ return client, nil
+}
+
+func (r *nativeRocketMQRuntime) newPullConsumer(topic string, consumerGroup string, tagExpression string, pullBatchSize int) (rocketmq.PullConsumer, error) {
+ group := strings.TrimSpace(consumerGroup)
+ if group == "" {
+ group = fmt.Sprintf("%s-%d", rocketMQDefaultConsumerGroup, time.Now().UnixNano())
+ }
+ if pullBatchSize <= 0 {
+ pullBatchSize = defaultRocketMQPullBatchSize
+ }
+ if pullBatchSize > maxRocketMQPullBatchSize {
+ pullBatchSize = maxRocketMQPullBatchSize
+ }
+ options := []rocketmqconsumer.Option{
+ rocketmqconsumer.WithNsResolver(rocketmqprimitive.NewPassthroughResolver(append([]string(nil), r.nameservers...))),
+ rocketmqconsumer.WithGroupName(group),
+ rocketmqconsumer.WithInstance(fmt.Sprintf("%s-consumer-%d", rocketMQDefaultInstancePrefix, time.Now().UnixNano())),
+ rocketmqconsumer.WithConsumeFromWhere(rocketmqconsumer.ConsumeFromFirstOffset),
+ rocketmqconsumer.WithPullBatchSize(int32(pullBatchSize)),
+ }
+ if namespace := strings.TrimSpace(r.namespace); namespace != "" {
+ options = append(options, rocketmqconsumer.WithNamespace(namespace))
+ }
+ if credentials, ok := rocketmqCredentials(r.config); ok {
+ options = append(options, rocketmqconsumer.WithCredentials(credentials))
+ }
+ client, err := rocketmq.NewPullConsumer(options...)
+ if err != nil {
+ return nil, err
+ }
+ selector := rocketmqconsumer.MessageSelector{
+ Type: rocketmqconsumer.TAG,
+ Expression: rocketmqNormalizeTagExpression(tagExpression),
+ }
+ if err := client.Subscribe(strings.TrimSpace(topic), selector); err != nil {
+ _ = client.Shutdown()
+ return nil, err
+ }
+ if err := client.Start(); err != nil {
+ _ = client.Shutdown()
+ return nil, err
+ }
+ return client, nil
+}
+
+func normalizeRocketMQConfig(config connection.ConnectionConfig) connection.ConnectionConfig {
+ runConfig := applyRocketMQURI(config)
+ if strings.TrimSpace(runConfig.Host) == "" && len(runConfig.Hosts) == 0 {
+ runConfig.Host = "localhost"
+ }
+ if runConfig.Port <= 0 {
+ runConfig.Port = defaultRocketMQPort
+ }
+ return runConfig
+}
+
+func applyRocketMQURI(config connection.ConnectionConfig) connection.ConnectionConfig {
+ uriText := strings.TrimSpace(config.URI)
+ if uriText == "" {
+ return config
+ }
+ parsed, err := url.Parse(uriText)
+ if err != nil {
+ return config
+ }
+ scheme := strings.ToLower(strings.TrimSpace(parsed.Scheme))
+ switch scheme {
+ case "rocketmq", "rocket-mq", "rocket_mq", "apache-rocketmq", "apache_rocketmq", "rmq":
+ default:
+ return config
+ }
+ if parsed.User != nil {
+ if strings.TrimSpace(config.User) == "" {
+ config.User = parsed.User.Username()
+ }
+ if pass, ok := parsed.User.Password(); ok && config.Password == "" {
+ config.Password = pass
+ }
+ }
+ hosts := make([]string, 0, 4)
+ for _, entry := range strings.Split(strings.TrimSpace(parsed.Host), ",") {
+ host, port, ok := parseHostPortWithDefault(strings.TrimSpace(entry), defaultRocketMQPort)
+ if !ok {
+ continue
+ }
+ hosts = append(hosts, rocketmqFormatHostPort(host, port))
+ }
+ if len(hosts) > 0 {
+ host, port, ok := parseHostPortWithDefault(hosts[0], defaultRocketMQPort)
+ if ok {
+ config.Host = host
+ config.Port = port
+ }
+ if len(hosts) > 1 {
+ config.Hosts = append([]string(nil), hosts[1:]...)
+ }
+ }
+ if topic := strings.Trim(strings.TrimSpace(parsed.Path), "/"); topic != "" && strings.TrimSpace(config.Database) == "" {
+ config.Database = topic
+ }
+ params := parsed.Query()
+ if strings.TrimSpace(config.Topology) == "" {
+ if topology := strings.ToLower(strings.TrimSpace(firstNonEmpty(params.Get("topology"), params.Get("mode")))); topology != "" {
+ config.Topology = topology
+ } else if len(hosts) > 1 {
+ config.Topology = "cluster"
+ }
+ }
+ return config
+}
+
+func rocketmqConnectionParams(config connection.ConnectionConfig) url.Values {
+ params := url.Values{}
+ mergeConnectionParamValues(params, connectionParamsFromURI(config.URI, "rocketmq", "rocket-mq", "rocket_mq", "apache-rocketmq", "apache_rocketmq", "rmq"))
+ mergeConnectionParamValues(params, connectionParamsFromText(config.ConnectionParams))
+ return params
+}
+
+func rocketmqDefaultTopic(config connection.ConnectionConfig) string {
+ if topic := strings.TrimSpace(config.Database); topic != "" {
+ return topic
+ }
+ params := rocketmqConnectionParams(config)
+ return firstNonEmpty(params.Get("topic"), params.Get("defaultTopic"), params.Get("default_topic"))
+}
+
+func rocketmqConfiguredConsumerGroup(config connection.ConnectionConfig) string {
+ params := rocketmqConnectionParams(config)
+ return firstNonEmpty(
+ params.Get("groupId"),
+ params.Get("group_id"),
+ params.Get("consumerGroup"),
+ params.Get("consumer_group"),
+ )
+}
+
+func rocketmqProducerGroup(config connection.ConnectionConfig) string {
+ params := rocketmqConnectionParams(config)
+ return firstNonEmpty(params.Get("producerGroup"), params.Get("producer_group"))
+}
+
+func rocketmqConfiguredTagExpression(config connection.ConnectionConfig) string {
+ params := rocketmqConnectionParams(config)
+ return firstNonEmpty(
+ params.Get("tag"),
+ params.Get("tags"),
+ params.Get("tagExpression"),
+ params.Get("tag_expression"),
+ params.Get("selector"),
+ params.Get("selectorExpression"),
+ params.Get("selector_expression"),
+ )
+}
+
+func rocketmqTagExpressionIsDefault(value string) bool {
+ text := strings.TrimSpace(value)
+ return text == "" || text == "*" || strings.EqualFold(text, "all")
+}
+
+func rocketmqNormalizeTagExpression(value string) string {
+ text := strings.TrimSpace(value)
+ if rocketmqTagExpressionIsDefault(text) {
+ return "*"
+ }
+ return text
+}
+
+func rocketmqNamespace(config connection.ConnectionConfig) string {
+ params := rocketmqConnectionParams(config)
+ return firstNonEmpty(params.Get("namespace"), params.Get("ns"))
+}
+
+func rocketmqDefaultStartLatest(config connection.ConnectionConfig) bool {
+ params := rocketmqConnectionParams(config)
+ value := strings.ToLower(strings.TrimSpace(firstNonEmpty(
+ params.Get("startOffset"),
+ params.Get("start_offset"),
+ params.Get("consumeFrom"),
+ params.Get("consume_from"),
+ )))
+ switch value {
+ case "latest", "last", "newest", "end", "tail":
+ return true
+ default:
+ return false
+ }
+}
+
+func rocketmqPullBatchSize(config connection.ConnectionConfig) int {
+ params := rocketmqConnectionParams(config)
+ value := strings.TrimSpace(firstNonEmpty(params.Get("pullBatchSize"), params.Get("pull_batch_size")))
+ if size, err := strconv.Atoi(value); err == nil && size > 0 {
+ if size > maxRocketMQPullBatchSize {
+ return maxRocketMQPullBatchSize
+ }
+ return size
+ }
+ return defaultRocketMQPullBatchSize
+}
+
+func rocketmqSendTimeout(config connection.ConnectionConfig) time.Duration {
+ params := rocketmqConnectionParams(config)
+ value := strings.TrimSpace(firstNonEmpty(params.Get("sendTimeoutMs"), params.Get("send_timeout_ms")))
+ if ms, err := strconv.Atoi(value); err == nil && ms > 0 {
+ return time.Duration(ms) * time.Millisecond
+ }
+ timeout := getConnectTimeout(config)
+ if timeout <= 0 {
+ timeout = 10 * time.Second
+ }
+ return timeout
+}
+
+func rocketmqCredentials(config connection.ConnectionConfig) (rocketmqprimitive.Credentials, bool) {
+ params := rocketmqConnectionParams(config)
+ accessKey := strings.TrimSpace(firstNonEmpty(config.User, params.Get("accessKey"), params.Get("access_key")))
+ secretKey := strings.TrimSpace(firstNonEmpty(config.Password, params.Get("secretKey"), params.Get("secret_key")))
+ securityToken := strings.TrimSpace(firstNonEmpty(params.Get("securityToken"), params.Get("security_token")))
+ credentials := rocketmqprimitive.Credentials{
+ AccessKey: accessKey,
+ SecretKey: secretKey,
+ SecurityToken: securityToken,
+ }
+ if credentials.IsEmpty() {
+ return rocketmqprimitive.Credentials{}, false
+ }
+ return credentials, true
+}
+
+func rocketmqNameServerAddresses(config connection.ConnectionConfig) ([]string, error) {
+ candidates := make([]string, 0, len(config.Hosts)+1)
+ if host := strings.TrimSpace(config.Host); host != "" {
+ port := config.Port
+ if port <= 0 {
+ port = defaultRocketMQPort
+ }
+ candidates = append(candidates, rocketmqFormatHostPort(host, port))
+ }
+ candidates = append(candidates, config.Hosts...)
+ seen := map[string]struct{}{}
+ nameservers := make([]string, 0, len(candidates))
+ for _, candidate := range candidates {
+ host, port, ok := parseHostPortWithDefault(candidate, defaultRocketMQPort)
+ if !ok {
+ continue
+ }
+ address := rocketmqFormatHostPort(host, port)
+ if _, exists := seen[address]; exists {
+ continue
+ }
+ seen[address] = struct{}{}
+ nameservers = append(nameservers, address)
+ }
+ if len(nameservers) == 0 {
+ return nil, fmt.Errorf("RocketMQ 至少需要一个 NameServer 地址")
+ }
+ return nameservers, nil
+}
+
+func rocketmqFormatHostPort(host string, port int) string {
+ h := strings.TrimSpace(host)
+ if strings.Contains(h, ":") && !strings.HasPrefix(h, "[") {
+ return fmt.Sprintf("[%s]:%d", h, port)
+ }
+ return fmt.Sprintf("%s:%d", h, port)
+}
+
+func rocketmqResolveTopic(topic string, fallback string) string {
+ if text := strings.TrimSpace(topic); text != "" {
+ return text
+ }
+ return strings.TrimSpace(fallback)
+}
+
+func rocketmqIsSystemTopic(topic string) bool {
+ name := strings.TrimSpace(topic)
+ if name == "" {
+ return false
+ }
+ switch {
+ case strings.HasPrefix(name, "%RETRY%"),
+ strings.HasPrefix(name, "%DLQ%"),
+ strings.HasPrefix(name, "rmq_sys_"),
+ strings.HasPrefix(name, "CID_RMQ_SYS_"):
+ return true
+ }
+ switch name {
+ case "TBW102", "SELF_TEST_TOPIC", "OFFSET_MOVED_EVENT", "SCHEDULE_TOPIC_XXXX", "RMQ_SYS_TRANS_HALF_TOPIC", "RMQ_SYS_TRACE_TOPIC", "TRANS_CHECK_MAX_TIME_TOPIC", "BenchmarkTest":
+ return true
+ default:
+ return false
+ }
+}
+
+func rocketmqKeysFromAny(value interface{}) ([]string, error) {
+ switch typed := value.(type) {
+ case nil:
+ return nil, nil
+ case string:
+ return rocketmqSplitKeys(typed), nil
+ case []string:
+ result := make([]string, 0, len(typed))
+ for _, item := range typed {
+ if text := strings.TrimSpace(item); text != "" {
+ result = append(result, text)
+ }
+ }
+ return result, nil
+ case []interface{}:
+ result := make([]string, 0, len(typed))
+ for _, item := range typed {
+ text := strings.TrimSpace(fmt.Sprintf("%v", item))
+ if text != "" {
+ result = append(result, text)
+ }
+ }
+ return result, nil
+ default:
+ text := strings.TrimSpace(fmt.Sprintf("%v", value))
+ if text == "" || text == "" {
+ return nil, nil
+ }
+ return rocketmqSplitKeys(text), nil
+ }
+}
+
+func rocketmqSplitKeys(text string) []string {
+ parts := strings.FieldsFunc(text, func(r rune) bool {
+ return r == ',' || r == ';' || r == '|' || r == '\n' || r == '\r' || r == '\t' || r == ' ' || r == ','
+ })
+ result := make([]string, 0, len(parts))
+ for _, part := range parts {
+ if normalized := strings.TrimSpace(part); normalized != "" {
+ result = append(result, normalized)
+ }
+ }
+ return result
+}
+
+func rocketmqPropertiesFromAny(value interface{}) (map[string]string, error) {
+ switch typed := value.(type) {
+ case nil:
+ return nil, nil
+ case map[string]string:
+ result := make(map[string]string, len(typed))
+ for key, item := range typed {
+ if strings.TrimSpace(key) != "" {
+ result[strings.TrimSpace(key)] = item
+ }
+ }
+ return result, nil
+ case map[string]interface{}:
+ result := make(map[string]string, len(typed))
+ for key, item := range typed {
+ normalizedKey := strings.TrimSpace(key)
+ if normalizedKey == "" {
+ continue
+ }
+ switch casted := item.(type) {
+ case string:
+ result[normalizedKey] = casted
+ default:
+ payload, err := json.Marshal(casted)
+ if err != nil {
+ return nil, fmt.Errorf("RocketMQ properties 字段 %q 无法序列化:%w", normalizedKey, err)
+ }
+ result[normalizedKey] = string(payload)
+ }
+ }
+ return result, nil
+ default:
+ return nil, fmt.Errorf("RocketMQ properties 必须是 JSON 对象")
+ }
+}
+
+func rocketmqDelayLevelFromAny(value interface{}) (int, error) {
+ switch typed := value.(type) {
+ case nil:
+ return 0, nil
+ case json.Number:
+ n, err := typed.Int64()
+ if err != nil {
+ return 0, fmt.Errorf("RocketMQ delayLevel 必须是正整数")
+ }
+ return rocketmqNormalizeDelayLevel(int(n))
+ case float64:
+ return rocketmqNormalizeDelayLevel(int(typed))
+ case int:
+ return rocketmqNormalizeDelayLevel(typed)
+ case int64:
+ return rocketmqNormalizeDelayLevel(int(typed))
+ case string:
+ text := strings.TrimSpace(typed)
+ if text == "" {
+ return 0, nil
+ }
+ n, err := strconv.Atoi(text)
+ if err != nil {
+ return 0, fmt.Errorf("RocketMQ delayLevel 必须是正整数")
+ }
+ return rocketmqNormalizeDelayLevel(n)
+ default:
+ return 0, fmt.Errorf("RocketMQ delayLevel 必须是正整数")
+ }
+}
+
+func rocketmqNormalizeDelayLevel(value int) (int, error) {
+ if value < 0 {
+ return 0, fmt.Errorf("RocketMQ delayLevel 必须是正整数")
+ }
+ return value, nil
+}
+
+func rocketmqRecordFromExt(message *rocketmqprimitive.MessageExt, brokerName string, queueID int, minOffset int64, maxOffset int64) rocketmqMessageRecord {
+ if message == nil {
+ return rocketmqMessageRecord{
+ BrokerName: brokerName,
+ QueueID: queueID,
+ MinOffset: minOffset,
+ MaxOffset: maxOffset,
+ }
+ }
+ decoded, encoding := mqttDecodePayload(message.Body)
+ embeddedBroker := ""
+ if message.Queue != nil {
+ embeddedBroker = message.Queue.BrokerName
+ }
+ return rocketmqMessageRecord{
+ Topic: message.Topic,
+ BrokerName: firstNonEmpty(brokerName, embeddedBroker),
+ QueueID: queueID,
+ QueueOffset: message.QueueOffset,
+ MsgID: message.MsgId,
+ OffsetMsgID: message.OffsetMsgId,
+ Tags: message.GetTags(),
+ Keys: message.GetKeys(),
+ Body: append([]byte(nil), message.Body...),
+ Decoded: decoded,
+ Encoding: encoding,
+ Properties: message.GetProperties(),
+ BornTimestamp: time.UnixMilli(message.BornTimestamp),
+ StoreTimestamp: time.UnixMilli(message.StoreTimestamp),
+ ReconsumeTimes: message.ReconsumeTimes,
+ MinOffset: minOffset,
+ MaxOffset: maxOffset,
+ }
+}
+
+func parseRocketMQSQL(sqlText string, defaultLatest bool) (rocketmqParsedSQL, bool) {
+ text := strings.TrimSpace(sqlText)
+ if text == "" {
+ return rocketmqParsedSQL{}, false
+ }
+ if matches := rocketmqShowTopicsRE.FindStringSubmatch(text); len(matches) > 0 {
+ parsed := rocketmqParsedSQL{Action: "show_topics"}
+ if len(matches) > 1 && strings.TrimSpace(matches[1]) != "" {
+ parsed.Limit, _ = strconv.Atoi(matches[1])
+ }
+ return parsed, true
+ }
+ if matches := rocketmqDescribeTopicRE.FindStringSubmatch(text); len(matches) > 0 {
+ return rocketmqParsedSQL{
+ Action: "describe_topic",
+ Topic: firstNonEmpty(matches[1], matches[2], matches[3]),
+ }, true
+ }
+ if matches := rocketmqConsumeTopicRE.FindStringSubmatch(text); len(matches) > 0 {
+ parsed := rocketmqParsedSQL{
+ Action: "consume",
+ Topic: firstNonEmpty(matches[1], matches[2], matches[3]),
+ Limit: defaultRocketMQPreviewLimit,
+ Latest: true,
+ }
+ if limitMatch := rocketmqSQLLimitRE.FindStringSubmatch(text); len(limitMatch) > 1 {
+ parsed.Limit, _ = strconv.Atoi(limitMatch[1])
+ }
+ if offsetMatch := rocketmqSQLOffsetRE.FindStringSubmatch(text); len(offsetMatch) > 1 {
+ parsed.Offset, _ = strconv.Atoi(offsetMatch[1])
+ }
+ return parsed, true
+ }
+ if !strings.HasPrefix(strings.ToLower(text), "select") {
+ return rocketmqParsedSQL{}, false
+ }
+ matches := rocketmqSQLFromRE.FindStringSubmatch(text)
+ if len(matches) == 0 {
+ return rocketmqParsedSQL{}, false
+ }
+ parsed := rocketmqParsedSQL{
+ Action: "select",
+ Topic: firstNonEmpty(matches[1], matches[2], matches[3]),
+ Limit: defaultRocketMQPreviewLimit,
+ Count: strings.Contains(strings.ToLower(text), "count("),
+ Latest: defaultLatest,
+ }
+ if limitMatch := rocketmqSQLLimitRE.FindStringSubmatch(text); len(limitMatch) > 1 {
+ parsed.Limit, _ = strconv.Atoi(limitMatch[1])
+ }
+ if offsetMatch := rocketmqSQLOffsetRE.FindStringSubmatch(text); len(offsetMatch) > 1 {
+ parsed.Offset, _ = strconv.Atoi(offsetMatch[1])
+ }
+ return parsed, true
+}
+
+type rocketmqParsedSQL struct {
+ Action string
+ Topic string
+ Limit int
+ Offset int
+ Count bool
+ Latest bool
+}
+
+var (
+ rocketmqSQLFromRE = regexp.MustCompile(`(?i)\bFROM\s+(?:"([^"]+)"|` + "`" + `([^` + "`" + `]+)` + "`" + `|([^\s;]+))`)
+ rocketmqSQLLimitRE = regexp.MustCompile(`(?i)\bLIMIT\s+(\d+)`)
+ rocketmqSQLOffsetRE = regexp.MustCompile(`(?i)\bOFFSET\s+(\d+)`)
+ rocketmqShowTopicsRE = regexp.MustCompile(`(?i)^\s*SHOW\s+TOPICS(?:\s+LIMIT\s+(\d+))?\s*;?\s*$`)
+ rocketmqDescribeTopicRE = regexp.MustCompile(`(?i)^\s*(?:SHOW|DESCRIBE)\s+TOPIC\s+(?:"([^"]+)"|` + "`" + `([^` + "`" + `]+)` + "`" + `|([^\s;]+))\s*;?\s*$`)
+ rocketmqConsumeTopicRE = regexp.MustCompile(`(?i)^\s*CONSUME\s+FROM\s+(?:"([^"]+)"|` + "`" + `([^` + "`" + `]+)` + "`" + `|([^\s;]+))`)
+)
+
+func rocketmqTopicRows(topics []rocketmqTopicInfo) []map[string]interface{} {
+ rows := make([]map[string]interface{}, 0, len(topics))
+ for _, topic := range topics {
+ rows = append(rows, map[string]interface{}{
+ "topic": topic.Name,
+ "system_topic": topic.System,
+ "queue_count": topic.QueueCount,
+ })
+ }
+ return rows
+}
+
+func rocketmqDescribeRows(description rocketmqTopicDescription) []map[string]interface{} {
+ rows := make([]map[string]interface{}, 0, len(description.Queues))
+ for _, queue := range description.Queues {
+ rows = append(rows, map[string]interface{}{
+ "topic": description.Name,
+ "namespace": description.Namespace,
+ "consumer_group": description.ConsumerGroup,
+ "tag_expression": description.TagExpression,
+ "queue_count": description.QueueCount,
+ "topic_approximate_count": description.TotalApproximateCount,
+ "broker_name": queue.BrokerName,
+ "queue_id": queue.QueueID,
+ "min_offset": queue.MinOffset,
+ "max_offset": queue.MaxOffset,
+ "approximate_count": queue.ApproximateCount,
+ })
+ }
+ if len(rows) == 0 {
+ rows = append(rows, map[string]interface{}{
+ "topic": description.Name,
+ "namespace": description.Namespace,
+ "consumer_group": description.ConsumerGroup,
+ "tag_expression": description.TagExpression,
+ "queue_count": 0,
+ "topic_approximate_count": 0,
+ })
+ }
+ return rows
+}
+
+func rocketmqMessageRows(records []rocketmqMessageRecord) []map[string]interface{} {
+ rows := make([]map[string]interface{}, 0, len(records))
+ for _, record := range records {
+ row := map[string]interface{}{
+ "topic": record.Topic,
+ "broker_name": record.BrokerName,
+ "queue_id": record.QueueID,
+ "queue_offset": record.QueueOffset,
+ "msg_id": record.MsgID,
+ "offset_msg_id": record.OffsetMsgID,
+ "tags": record.Tags,
+ "keys": record.Keys,
+ "born_timestamp": record.BornTimestamp,
+ "store_timestamp": record.StoreTimestamp,
+ "reconsume_times": record.ReconsumeTimes,
+ "body": record.Decoded,
+ "body_encoding": record.Encoding,
+ "properties": record.Properties,
+ "min_offset": record.MinOffset,
+ "max_offset": record.MaxOffset,
+ }
+ if payloadMap, ok := record.Decoded.(map[string]interface{}); ok {
+ flattenRocketMQMap("body", payloadMap, row)
+ }
+ if len(record.Properties) > 0 {
+ for key, value := range record.Properties {
+ if strings.TrimSpace(key) == "" {
+ continue
+ }
+ row["properties."+key] = value
+ }
+ }
+ rows = append(rows, row)
+ }
+ return rows
+}
+
+func flattenRocketMQMap(prefix string, values map[string]interface{}, row map[string]interface{}) {
+ for key, value := range values {
+ if strings.TrimSpace(key) == "" {
+ continue
+ }
+ name := prefix + "." + key
+ row[name] = value
+ if nested, ok := value.(map[string]interface{}); ok {
+ flattenRocketMQMap(name, nested, row)
+ }
+ }
+}
+
+func rocketmqSortMessages(records []rocketmqMessageRecord, latest bool) {
+ sort.Slice(records, func(i, j int) bool {
+ left := records[i]
+ right := records[j]
+ switch {
+ case left.StoreTimestamp.Equal(right.StoreTimestamp):
+ if left.QueueOffset == right.QueueOffset {
+ if left.QueueID == right.QueueID {
+ return left.BrokerName < right.BrokerName
+ }
+ if latest {
+ return left.QueueID > right.QueueID
+ }
+ return left.QueueID < right.QueueID
+ }
+ if latest {
+ return left.QueueOffset > right.QueueOffset
+ }
+ return left.QueueOffset < right.QueueOffset
+ case latest:
+ return left.StoreTimestamp.After(right.StoreTimestamp)
+ default:
+ return left.StoreTimestamp.Before(right.StoreTimestamp)
+ }
+ })
+}
diff --git a/internal/db/rocketmq_impl_test.go b/internal/db/rocketmq_impl_test.go
new file mode 100644
index 0000000..f719aee
--- /dev/null
+++ b/internal/db/rocketmq_impl_test.go
@@ -0,0 +1,312 @@
+package db
+
+import (
+ "context"
+ "reflect"
+ "strings"
+ "testing"
+ "time"
+
+ "GoNavi-Wails/internal/connection"
+
+ rocketmqprimitive "github.com/apache/rocketmq-client-go/v2/primitive"
+)
+
+type fakeRocketMQRuntime struct {
+ listTopicsResult []rocketmqTopicInfo
+ describeResult rocketmqTopicDescription
+ fetchResult []rocketmqMessageRecord
+ publishAffected int64
+ lastDescribe rocketmqDescribeRequest
+ lastFetch rocketmqFetchRequest
+ lastPublish rocketmqPublishCommand
+}
+
+func (f *fakeRocketMQRuntime) Close() error { return nil }
+
+func (f *fakeRocketMQRuntime) Ping(ctx context.Context) error { return nil }
+
+func (f *fakeRocketMQRuntime) ListTopics(ctx context.Context, includeSystem bool) ([]rocketmqTopicInfo, error) {
+ result := make([]rocketmqTopicInfo, 0, len(f.listTopicsResult))
+ for _, item := range f.listTopicsResult {
+ if item.System && !includeSystem {
+ continue
+ }
+ result = append(result, item)
+ }
+ return result, nil
+}
+
+func (f *fakeRocketMQRuntime) DescribeTopic(ctx context.Context, request rocketmqDescribeRequest) (rocketmqTopicDescription, error) {
+ f.lastDescribe = request
+ return f.describeResult, nil
+}
+
+func (f *fakeRocketMQRuntime) FetchMessages(ctx context.Context, request rocketmqFetchRequest) ([]rocketmqMessageRecord, error) {
+ f.lastFetch = request
+ items := append([]rocketmqMessageRecord(nil), f.fetchResult...)
+ if request.Offset > 0 {
+ if request.Offset >= len(items) {
+ return []rocketmqMessageRecord{}, nil
+ }
+ items = items[request.Offset:]
+ }
+ if request.Limit > 0 && len(items) > request.Limit {
+ items = items[:request.Limit]
+ }
+ return items, nil
+}
+
+func (f *fakeRocketMQRuntime) Publish(ctx context.Context, command rocketmqPublishCommand) (int64, error) {
+ f.lastPublish = command
+ return f.publishAffected, nil
+}
+
+func TestNormalizeRocketMQConfigParsesURIAndParams(t *testing.T) {
+ config := normalizeRocketMQConfig(connection.ConnectionConfig{
+ URI: "rocketmq://ak:sk@127.0.0.1:9876,127.0.0.2:9877/orders.events?topology=cluster&groupId=preview&namespace=prod&tag=TagA&pullBatchSize=64&startOffset=latest",
+ ConnectionParams: "producerGroup=writer&sendTimeoutMs=6000",
+ })
+
+ if config.Host != "127.0.0.1" || config.Port != 9876 {
+ t.Fatalf("unexpected rocketmq host/port: %#v", config)
+ }
+ if !reflect.DeepEqual(config.Hosts, []string{"127.0.0.2:9877"}) {
+ t.Fatalf("unexpected rocketmq extra nameservers: %#v", config.Hosts)
+ }
+ if config.User != "ak" || config.Password != "sk" {
+ t.Fatalf("unexpected rocketmq credentials: %#v", config)
+ }
+ if config.Database != "orders.events" || config.Topology != "cluster" {
+ t.Fatalf("unexpected rocketmq topic/topology: %#v", config)
+ }
+
+ params := rocketmqConnectionParams(config)
+ if params.Get("groupId") != "preview" || params.Get("namespace") != "prod" || params.Get("tag") != "TagA" {
+ t.Fatalf("unexpected rocketmq params: %#v", params)
+ }
+ if params.Get("producerGroup") != "writer" || params.Get("sendTimeoutMs") != "6000" {
+ t.Fatalf("unexpected rocketmq producer params: %#v", params)
+ }
+}
+
+func TestRocketMQQueryExecAndColumns(t *testing.T) {
+ fakeRuntime := &fakeRocketMQRuntime{
+ listTopicsResult: []rocketmqTopicInfo{
+ {Name: "orders.events", QueueCount: 2},
+ {Name: "%RETRY%preview", System: true, QueueCount: 1},
+ },
+ describeResult: rocketmqTopicDescription{
+ Name: "orders.events",
+ Namespace: "prod",
+ ConsumerGroup: "preview",
+ TagExpression: "*",
+ QueueCount: 2,
+ TotalApproximateCount: 42,
+ Queues: []rocketmqTopicQueueInfo{
+ {BrokerName: "broker-a", QueueID: 0, MinOffset: 0, MaxOffset: 21, ApproximateCount: 21},
+ {BrokerName: "broker-b", QueueID: 1, MinOffset: 0, MaxOffset: 21, ApproximateCount: 21},
+ },
+ },
+ fetchResult: []rocketmqMessageRecord{
+ {
+ Topic: "orders.events",
+ BrokerName: "broker-a",
+ QueueID: 0,
+ QueueOffset: 11,
+ MsgID: "msg-11",
+ OffsetMsgID: "offset-11",
+ Tags: "TagA",
+ Keys: "order-11 tenant-a",
+ Decoded: map[string]interface{}{"event": "created", "meta": map[string]interface{}{"source": "erp"}},
+ Encoding: "json",
+ Properties: map[string]string{"trace": "trace-11"},
+ BornTimestamp: time.Date(2026, 6, 14, 12, 0, 0, 0, time.UTC),
+ StoreTimestamp: time.Date(2026, 6, 14, 12, 0, 1, 0, time.UTC),
+ },
+ {
+ Topic: "orders.events",
+ BrokerName: "broker-b",
+ QueueID: 1,
+ QueueOffset: 12,
+ MsgID: "msg-12",
+ OffsetMsgID: "offset-12",
+ Tags: "TagB",
+ Keys: "order-12",
+ Decoded: "plain-text",
+ Encoding: "text",
+ Properties: map[string]string{"trace": "trace-12"},
+ BornTimestamp: time.Date(2026, 6, 14, 12, 0, 2, 0, time.UTC),
+ StoreTimestamp: time.Date(2026, 6, 14, 12, 0, 3, 0, time.UTC),
+ },
+ },
+ publishAffected: 1,
+ }
+
+ originalFactory := newRocketMQRuntime
+ newRocketMQRuntime = func(config connection.ConnectionConfig) (rocketmqRuntime, error) {
+ return fakeRuntime, nil
+ }
+ defer func() {
+ newRocketMQRuntime = originalFactory
+ }()
+
+ client := &RocketMQDB{}
+ if err := client.Connect(connection.ConnectionConfig{
+ Type: "rocketmq",
+ Host: "127.0.0.1",
+ Port: 9876,
+ Database: "orders.events",
+ ConnectionParams: "groupId=preview&namespace=prod&pullBatchSize=48&startOffset=latest",
+ }); err != nil {
+ t.Fatalf("Connect failed: %v", err)
+ }
+ defer client.Close()
+
+ rows, columns, err := client.Query(`SHOW TOPICS LIMIT 1`)
+ if err != nil {
+ t.Fatalf("SHOW TOPICS failed: %v", err)
+ }
+ if len(rows) != 1 || rows[0]["topic"] != "orders.events" || rows[0]["queue_count"] != 2 {
+ t.Fatalf("unexpected rocketmq topic rows: %#v", rows)
+ }
+ if !containsString(columns, "system_topic") {
+ t.Fatalf("expected system_topic column, got %v", columns)
+ }
+
+ rows, columns, err = client.Query(`DESCRIBE TOPIC "orders.events"`)
+ if err != nil {
+ t.Fatalf("DESCRIBE TOPIC failed: %v", err)
+ }
+ if fakeRuntime.lastDescribe.Topic != "orders.events" || fakeRuntime.lastDescribe.ConsumerGroup != "preview" {
+ t.Fatalf("unexpected describe request: %#v", fakeRuntime.lastDescribe)
+ }
+ if len(rows) != 2 || rows[0]["topic_approximate_count"] != int64(42) {
+ t.Fatalf("unexpected rocketmq describe rows: %#v", rows)
+ }
+ if !containsString(columns, "broker_name") {
+ t.Fatalf("expected broker_name column, got %v", columns)
+ }
+
+ rows, columns, err = client.Query(`SELECT * FROM "orders.events" LIMIT 1 OFFSET 1`)
+ if err != nil {
+ t.Fatalf("SELECT topic failed: %v", err)
+ }
+ if fakeRuntime.lastFetch.Topic != "orders.events" || fakeRuntime.lastFetch.Limit != 1 || fakeRuntime.lastFetch.Offset != 1 || !fakeRuntime.lastFetch.Latest {
+ t.Fatalf("unexpected fetch request: %#v", fakeRuntime.lastFetch)
+ }
+ if len(rows) != 1 || rows[0]["body"] != "plain-text" || rows[0]["properties.trace"] != "trace-12" {
+ t.Fatalf("unexpected rocketmq message rows: %#v", rows)
+ }
+ if !containsString(columns, "body_encoding") || !containsString(columns, "properties.trace") {
+ t.Fatalf("unexpected columns: %v", columns)
+ }
+
+ rows, columns, err = client.Query(`SELECT COUNT(*) FROM "orders.events"`)
+ if err != nil {
+ t.Fatalf("COUNT(*) failed: %v", err)
+ }
+ if len(rows) != 1 || rows[0]["total_approximate_count"] != int64(42) {
+ t.Fatalf("unexpected count rows: %#v", rows)
+ }
+ if !containsString(columns, "queue_count") {
+ t.Fatalf("expected queue_count column, got %v", columns)
+ }
+
+ affected, err := client.Exec(`{"publish":"orders.events","payload":{"id":1},"tag":"TagA","keys":["order-1","tenant-a"],"delayLevel":3,"properties":{"trace":"trace-1"}}`)
+ if err != nil {
+ t.Fatalf("RocketMQ publish failed: %v", err)
+ }
+ if affected != 1 {
+ t.Fatalf("unexpected affected rows: %d", affected)
+ }
+ if fakeRuntime.lastPublish.Topic != "orders.events" || fakeRuntime.lastPublish.Tag != "TagA" || fakeRuntime.lastPublish.DelayLevel != 3 {
+ t.Fatalf("unexpected publish command: %#v", fakeRuntime.lastPublish)
+ }
+ if !reflect.DeepEqual(fakeRuntime.lastPublish.Keys, []string{"order-1", "tenant-a"}) {
+ t.Fatalf("unexpected publish keys: %#v", fakeRuntime.lastPublish.Keys)
+ }
+ if fakeRuntime.lastPublish.Properties["trace"] != "trace-1" {
+ t.Fatalf("unexpected publish properties: %#v", fakeRuntime.lastPublish.Properties)
+ }
+
+ columnDefs, err := client.GetColumns(rocketMQSyntheticDatabase, "orders.events")
+ if err != nil {
+ t.Fatalf("GetColumns failed: %v", err)
+ }
+ names := make([]string, 0, len(columnDefs))
+ for _, col := range columnDefs {
+ names = append(names, col.Name)
+ }
+ joined := strings.Join(names, ",")
+ for _, want := range []string{"topic", "body.meta.source", "properties.trace"} {
+ if !strings.Contains(joined, want) {
+ t.Fatalf("expected rocketmq column %q in %s", want, joined)
+ }
+ }
+
+ databases, err := client.GetDatabases()
+ if err != nil {
+ t.Fatalf("GetDatabases failed: %v", err)
+ }
+ if !reflect.DeepEqual(databases, []string{rocketMQSyntheticDatabase}) {
+ t.Fatalf("unexpected rocketmq database list: %#v", databases)
+ }
+
+ tables, err := client.GetTables(rocketMQSyntheticDatabase)
+ if err != nil {
+ t.Fatalf("GetTables failed: %v", err)
+ }
+ if !reflect.DeepEqual(tables, []string{"orders.events"}) {
+ t.Fatalf("unexpected rocketmq topic list: %#v", tables)
+ }
+}
+
+func TestRocketMQCountRejectsTagFilteredConnections(t *testing.T) {
+ fakeRuntime := &fakeRocketMQRuntime{}
+
+ originalFactory := newRocketMQRuntime
+ newRocketMQRuntime = func(config connection.ConnectionConfig) (rocketmqRuntime, error) {
+ return fakeRuntime, nil
+ }
+ defer func() {
+ newRocketMQRuntime = originalFactory
+ }()
+
+ client := &RocketMQDB{}
+ if err := client.Connect(connection.ConnectionConfig{
+ Type: "rocketmq",
+ Host: "127.0.0.1",
+ Port: 9876,
+ Database: "orders.events",
+ ConnectionParams: "tag=TagA",
+ }); err != nil {
+ t.Fatalf("Connect failed: %v", err)
+ }
+ defer client.Close()
+
+ if _, _, err := client.Query(`SELECT COUNT(*) FROM "orders.events"`); err == nil || !strings.Contains(err.Error(), "TAG 过滤") {
+ t.Fatalf("expected COUNT(*) to be rejected for tag-filtered RocketMQ, got %v", err)
+ }
+}
+
+func TestRocketMQRecordFromExtUsesPayloadDecoder(t *testing.T) {
+ record := rocketmqRecordFromExt(&rocketmqprimitive.MessageExt{
+ Message: rocketmqprimitive.Message{
+ Topic: "orders.events",
+ Body: []byte(`{"id":1}`),
+ },
+ MsgId: "msg-1",
+ OffsetMsgId: "offset-1",
+ QueueOffset: 2,
+ BornTimestamp: time.Date(2026, 6, 14, 12, 0, 0, 0, time.UTC).UnixMilli(),
+ StoreTimestamp: time.Date(2026, 6, 14, 12, 0, 1, 0, time.UTC).UnixMilli(),
+ }, "broker-a", 1, 0, 3)
+
+ if record.Encoding != "json" {
+ t.Fatalf("expected json encoding, got %#v", record)
+ }
+ if body, ok := record.Decoded.(map[string]interface{}); !ok || body["id"] == nil {
+ t.Fatalf("unexpected decoded body: %#v", record.Decoded)
+ }
+}