mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-22 17:00:21 +08:00
✨ feat(starrocks): 新增 StarRocks 数据源与高级对象能力
- 后端接入:新增独立 starrocks 可选驱动,复用 MySQL wire 协议并支持默认 9030 端口 - 驱动管理:补齐 manifest、build tag、revision、driver-agent provider 和构建脚本 - 前端接入:新增 StarRocks 连接类型、图标、能力矩阵、URI 解析、保存回显和 SQL 自动 LIMIT - 方言增强:新增 StarRocks 类型、关键字、函数补全和专属建表 SQL 生成 - 高级对象:支持物化视图对象浏览、Rollup 模板、外部 Catalog 模板和高级表设计器参数 - CI 发布:将 StarRocks driver-agent 纳入 dev/release 构建与 release 资产校验
This commit is contained in:
2
.github/workflows/dev-build.yml
vendored
2
.github/workflows/dev-build.yml
vendored
@@ -261,7 +261,7 @@ jobs:
|
||||
TARGET_PLATFORM="${{ matrix.platform }}"
|
||||
GOOS="${TARGET_PLATFORM%%/*}"
|
||||
GOARCH="${TARGET_PLATFORM##*/}"
|
||||
DRIVERS=(mariadb oceanbase doris sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss mongodb tdengine clickhouse)
|
||||
DRIVERS=(mariadb oceanbase doris starrocks sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss mongodb tdengine clickhouse)
|
||||
OUTDIR="drivers/${{ matrix.os_name }}"
|
||||
mkdir -p "$OUTDIR"
|
||||
DUCKDB_WINDOWS_LIBRARY_VERSION="v1.4.4"
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -252,7 +252,7 @@ jobs:
|
||||
TARGET_PLATFORM="${{ matrix.platform }}"
|
||||
GOOS="${TARGET_PLATFORM%%/*}"
|
||||
GOARCH="${TARGET_PLATFORM##*/}"
|
||||
DRIVERS=(mariadb oceanbase doris sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss mongodb tdengine clickhouse)
|
||||
DRIVERS=(mariadb oceanbase doris starrocks sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss mongodb tdengine clickhouse)
|
||||
OUTDIR="drivers/${{ matrix.os_name }}"
|
||||
mkdir -p "$OUTDIR"
|
||||
DUCKDB_WINDOWS_LIBRARY_VERSION="v1.4.4"
|
||||
@@ -577,6 +577,10 @@ jobs:
|
||||
"drivers/MacOS/clickhouse-driver-agent-darwin-amd64"
|
||||
"drivers/MacOS/clickhouse-driver-agent-darwin-arm64"
|
||||
"drivers/Linux/clickhouse-driver-agent-linux-amd64"
|
||||
"drivers/Windows/starrocks-driver-agent-windows-amd64.exe"
|
||||
"drivers/MacOS/starrocks-driver-agent-darwin-amd64"
|
||||
"drivers/MacOS/starrocks-driver-agent-darwin-arm64"
|
||||
"drivers/Linux/starrocks-driver-agent-linux-amd64"
|
||||
)
|
||||
|
||||
missing=0
|
||||
|
||||
@@ -39,6 +39,7 @@ GoNavi is designed for developers and DBAs who need a unified desktop experience
|
||||
| Cache | Redis | Built-in | Key browsing, command execution, encoding/view switch |
|
||||
| Relational | MariaDB | Optional driver agent | Querying, object management, data editing |
|
||||
| Relational | Doris | Optional driver agent | Querying, object browsing, SQL execution |
|
||||
| Columnar Analytics | StarRocks | Optional driver agent | Querying, object browsing, SQL execution |
|
||||
| Search | Sphinx | Optional driver agent | SphinxQL querying and object browsing |
|
||||
| Relational | SQL Server | Optional driver agent | Schema browsing, SQL query, object management |
|
||||
| File-based | SQLite | Optional driver agent | Local DB browsing, editing, export |
|
||||
|
||||
@@ -38,6 +38,7 @@ GoNavi 面向开发者与 DBA,核心目标是让数据库操作在桌面端做
|
||||
| 缓存 | Redis | 内置 | Key 浏览、命令执行、编码/视图切换 |
|
||||
| 关系型 | MariaDB | 可选驱动代理 | 连接查询、对象管理、数据编辑 |
|
||||
| 关系型 | Doris | 可选驱动代理 | 连接查询、对象浏览、SQL 执行 |
|
||||
| 列式分析 | StarRocks | 可选驱动代理 | 连接查询、对象浏览、SQL 执行 |
|
||||
| 搜索 | Sphinx | 可选驱动代理 | SphinxQL 查询与对象浏览 |
|
||||
| 关系型 | SQL Server | 可选驱动代理 | 库表浏览、SQL 查询、对象管理 |
|
||||
| 文件型 | SQLite | 可选驱动代理 | 本地文件库浏览、编辑、导出 |
|
||||
|
||||
@@ -5,7 +5,7 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
DEFAULT_DRIVERS=(mariadb oceanbase doris sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss mongodb tdengine clickhouse)
|
||||
DEFAULT_DRIVERS=(mariadb oceanbase doris starrocks sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss mongodb tdengine clickhouse)
|
||||
DEFAULT_PLATFORMS=(darwin/amd64 darwin/arm64 windows/amd64 windows/arm64 linux/amd64 linux/arm64)
|
||||
DUCKDB_WINDOWS_LIBRARY_VERSION="v1.4.4"
|
||||
DUCKDB_WINDOWS_LIBRARY_URL="https://github.com/duckdb/duckdb/releases/download/${DUCKDB_WINDOWS_LIBRARY_VERSION}/libduckdb-windows-amd64.zip"
|
||||
@@ -42,7 +42,7 @@ normalize_driver() {
|
||||
case "$name" in
|
||||
doris|diros) echo "doris" ;;
|
||||
open_gauss|open-gauss) echo "opengauss" ;;
|
||||
mariadb|oceanbase|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|mongodb|tdengine|clickhouse)
|
||||
mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|mongodb|tdengine|clickhouse)
|
||||
echo "$name"
|
||||
;;
|
||||
*)
|
||||
|
||||
12
cmd/optional-driver-agent/provider_starrocks.go
Normal file
12
cmd/optional-driver-agent/provider_starrocks.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build gonavi_starrocks_driver
|
||||
|
||||
package main
|
||||
|
||||
import "GoNavi-Wails/internal/db"
|
||||
|
||||
func init() {
|
||||
agentDriverType = "starrocks"
|
||||
agentDatabaseFactory = func() db.Database {
|
||||
return &db.StarRocksDB{}
|
||||
}
|
||||
}
|
||||
@@ -202,6 +202,7 @@ const getDefaultPortByType = (type: string) => {
|
||||
return 2881;
|
||||
case "doris":
|
||||
case "diros":
|
||||
case "starrocks":
|
||||
return 9030;
|
||||
case "sphinx":
|
||||
return 9306;
|
||||
@@ -259,6 +260,7 @@ const sslSupportedTypes = new Set([
|
||||
"oceanbase",
|
||||
"doris",
|
||||
"diros",
|
||||
"starrocks",
|
||||
"sphinx",
|
||||
"dameng",
|
||||
"clickhouse",
|
||||
@@ -290,6 +292,7 @@ const isMySQLCompatibleType = (type: string) =>
|
||||
type === "oceanbase" ||
|
||||
type === "doris" ||
|
||||
type === "diros" ||
|
||||
type === "starrocks" ||
|
||||
type === "sphinx";
|
||||
|
||||
const supportsConnectionParamsForType = (type: string) =>
|
||||
@@ -1359,6 +1362,8 @@ const ConnectionModal: React.FC<{
|
||||
parseMultiHostUri(trimmedUri, "jdbc:mysql") ||
|
||||
parseMultiHostUri(trimmedUri, "oceanbase") ||
|
||||
parseMultiHostUri(trimmedUri, "jdbc:oceanbase") ||
|
||||
parseMultiHostUri(trimmedUri, "starrocks") ||
|
||||
parseMultiHostUri(trimmedUri, "jdbc:starrocks") ||
|
||||
parseMultiHostUri(trimmedUri, "diros") ||
|
||||
parseMultiHostUri(trimmedUri, "doris");
|
||||
if (!parsed) {
|
||||
@@ -1782,7 +1787,7 @@ const ConnectionModal: React.FC<{
|
||||
if (isMySQLCompatibleType(dbType)) {
|
||||
const defaultPort = getDefaultPortByType(dbType);
|
||||
const scheme =
|
||||
dbType === "diros" ? "doris" : dbType === "oceanbase" ? "oceanbase" : "mysql";
|
||||
dbType === "diros" ? "doris" : dbType === "starrocks" ? "starrocks" : dbType === "oceanbase" ? "oceanbase" : "mysql";
|
||||
if (dbType === "oceanbase") {
|
||||
return `${scheme}://sys%40oracle001:pass@127.0.0.1:${defaultPort}/SERVICE_NAME?protocol=oracle`;
|
||||
}
|
||||
@@ -1896,7 +1901,7 @@ const ConnectionModal: React.FC<{
|
||||
const dbPath = database ? `/${encodeURIComponent(database)}` : "/";
|
||||
const query = params.toString();
|
||||
const scheme =
|
||||
type === "diros" ? "doris" : type === "oceanbase" ? "oceanbase" : "mysql";
|
||||
type === "diros" ? "doris" : type === "starrocks" ? "starrocks" : type === "oceanbase" ? "oceanbase" : "mysql";
|
||||
return `${scheme}://${encodedAuth}${hosts.join(",")}${dbPath}${query ? `?${query}` : ""}`;
|
||||
}
|
||||
|
||||
@@ -2256,6 +2261,7 @@ const ConnectionModal: React.FC<{
|
||||
configType === "mariadb" ||
|
||||
configType === "oceanbase" ||
|
||||
configType === "diros" ||
|
||||
configType === "starrocks" ||
|
||||
configType === "sphinx"
|
||||
? normalizedHosts.slice(1)
|
||||
: [];
|
||||
@@ -3631,6 +3637,11 @@ const ConnectionModal: React.FC<{
|
||||
name: "Doris",
|
||||
icon: getDbIcon("diros", undefined, 36),
|
||||
},
|
||||
{
|
||||
key: "starrocks",
|
||||
name: "StarRocks",
|
||||
icon: getDbIcon("starrocks", undefined, 36),
|
||||
},
|
||||
{
|
||||
key: "sphinx",
|
||||
name: "Sphinx",
|
||||
|
||||
@@ -50,7 +50,7 @@ const quoteSqlIdent = (dbType: string, ident: string): string => {
|
||||
const raw = String(ident || '').trim();
|
||||
if (!raw) return raw;
|
||||
const t = String(dbType || '').toLowerCase();
|
||||
if (t === 'mysql' || t === 'mariadb' || t === 'oceanbase' || t === 'diros' || t === 'sphinx' || t === 'clickhouse' || t === 'tdengine') {
|
||||
if (t === 'mysql' || t === 'mariadb' || t === 'oceanbase' || t === 'diros' || t === 'starrocks' || t === 'sphinx' || t === 'clickhouse' || t === 'tdengine') {
|
||||
return `\`${raw.replace(/`/g, '``')}\``;
|
||||
}
|
||||
if (t === 'sqlserver') {
|
||||
|
||||
@@ -29,6 +29,7 @@ const DB_DEFAULT_COLORS: Record<string, string> = {
|
||||
highgo: '#00A86B',
|
||||
tdengine: '#2962FF',
|
||||
diros: '#0050B3',
|
||||
starrocks: '#00A6A6',
|
||||
sphinx: '#2F5D62',
|
||||
custom: '#888888',
|
||||
};
|
||||
@@ -121,6 +122,9 @@ const SQLServerIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
|
||||
const DorisIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
|
||||
<BrandSvgIcon type="diros" size={size} color={color} />
|
||||
);
|
||||
const StarRocksIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
|
||||
<ColorBadge size={size} color={color || DB_DEFAULT_COLORS.starrocks} label="SR" />
|
||||
);
|
||||
const SphinxIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
|
||||
<BrandSvgIcon type="sphinx" size={size} color={color} />
|
||||
);
|
||||
@@ -175,6 +179,7 @@ const DB_ICON_MAP: Record<string, React.FC<DbIconProps>> = {
|
||||
mariadb: MariaDBIcon,
|
||||
oceanbase: OceanBaseIcon,
|
||||
diros: DorisIcon,
|
||||
starrocks: StarRocksIcon,
|
||||
sphinx: SphinxIcon,
|
||||
postgres: PostgresIcon,
|
||||
redis: RedisIcon,
|
||||
@@ -197,7 +202,7 @@ const DB_ICON_MAP: Record<string, React.FC<DbIconProps>> = {
|
||||
/** 可选图标类型列表(用于图标选择器 UI) */
|
||||
export const DB_ICON_TYPES: string[] = [
|
||||
'mysql', 'mariadb', 'oceanbase', 'postgres', 'redis', 'mongodb', 'jvm',
|
||||
'oracle', 'sqlserver', 'sqlite', 'duckdb', 'clickhouse',
|
||||
'oracle', 'sqlserver', 'sqlite', 'duckdb', 'clickhouse', 'starrocks',
|
||||
'kingbase', 'dameng', 'vastbase', 'opengauss', 'highgo', 'tdengine', 'custom',
|
||||
];
|
||||
|
||||
@@ -218,6 +223,7 @@ export const getDbIconLabel = (type: string): string => {
|
||||
redis: 'Redis', mongodb: 'MongoDB', jvm: 'JVM',
|
||||
oracle: 'Oracle',
|
||||
sqlserver: 'SQL Server', clickhouse: 'ClickHouse', sqlite: 'SQLite',
|
||||
starrocks: 'StarRocks',
|
||||
duckdb: 'DuckDB', kingbase: '金仓', dameng: '达梦',
|
||||
vastbase: 'VastBase', opengauss: 'OpenGauss', highgo: '瀚高', tdengine: 'TDengine',
|
||||
custom: '自定义',
|
||||
|
||||
@@ -120,13 +120,23 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
return `CREATE OR REPLACE MACRO ${qualifiedName}(${parameters}) AS ${macroDefinition};`;
|
||||
};
|
||||
|
||||
const buildShowViewQueries = (dialect: string, viewName: string, dbName: string): string[] => {
|
||||
const buildShowViewQueries = (dialect: string, viewName: string, dbName: string, viewKind?: string): string[] => {
|
||||
const { schema, name } = parseSchemaAndName(viewName);
|
||||
const safeName = escapeSQLLiteral(name);
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks':
|
||||
if (dialect === 'starrocks' && viewKind === 'materialized') {
|
||||
const mvRef = schema
|
||||
? `\`${schema.replace(/`/g, '``')}\`.\`${name.replace(/`/g, '``')}\``
|
||||
: `\`${name.replace(/`/g, '``')}\``;
|
||||
return [
|
||||
`SHOW CREATE MATERIALIZED VIEW ${mvRef}`,
|
||||
`SHOW CREATE TABLE ${mvRef}`,
|
||||
];
|
||||
}
|
||||
return [
|
||||
`SHOW CREATE VIEW \`${name.replace(/`/g, '``')}\``,
|
||||
safeDbName
|
||||
@@ -172,6 +182,7 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks':
|
||||
return [
|
||||
`SHOW CREATE ${upperType} \`${name.replace(/`/g, '``')}\``,
|
||||
safeDbName
|
||||
@@ -277,7 +288,8 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
const row = data[0];
|
||||
|
||||
switch (dialect) {
|
||||
case 'mysql': {
|
||||
case 'mysql':
|
||||
case 'starrocks': {
|
||||
const keys = Object.keys(row);
|
||||
const textDefinition = row.view_definition || row.VIEW_DEFINITION;
|
||||
if (textDefinition) return normalizeMySQLViewDDL(textDefinition);
|
||||
@@ -305,7 +317,8 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
if (!data || data.length === 0) return '-- 未找到函数/存储过程定义';
|
||||
|
||||
switch (dialect) {
|
||||
case 'mysql': {
|
||||
case 'mysql':
|
||||
case 'starrocks': {
|
||||
const row = data[0];
|
||||
const keys = Object.keys(row);
|
||||
if (row.routine_definition || row.ROUTINE_DEFINITION) {
|
||||
@@ -380,9 +393,9 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
queries = buildShowViewQueries(dialect, viewName, dbName);
|
||||
queries = buildShowViewQueries(dialect, viewName, dbName, tab.viewKind);
|
||||
extractFn = extractViewDefinition;
|
||||
objectLabel = '视图';
|
||||
objectLabel = tab.viewKind === 'materialized' ? '物化视图' : '视图';
|
||||
} else {
|
||||
const routineName = tab.routineName || '';
|
||||
const routineType = tab.routineType || 'FUNCTION';
|
||||
@@ -443,9 +456,9 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
};
|
||||
|
||||
loadDefinition();
|
||||
}, [tab.connectionId, tab.dbName, tab.viewName, tab.routineName, tab.routineType, tab.type, connections]);
|
||||
}, [tab.connectionId, tab.dbName, tab.viewName, tab.viewKind, tab.routineName, tab.routineType, tab.type, connections]);
|
||||
|
||||
const objectLabel = tab.type === 'view-def' ? '视图' : '函数/存储过程';
|
||||
const objectLabel = tab.type === 'view-def' ? (tab.viewKind === 'materialized' ? '物化视图' : '视图') : '函数/存储过程';
|
||||
const objectName = tab.type === 'view-def' ? tab.viewName : tab.routineName;
|
||||
|
||||
if (loading) {
|
||||
|
||||
@@ -48,6 +48,7 @@ import FindInDatabaseModal from './FindInDatabaseModal';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { noAutoCapInputProps } from '../utils/inputAutoCap';
|
||||
import { normalizeSidebarViewName, resolveSidebarRuntimeDatabase } from '../utils/sidebarMetadata';
|
||||
import { buildStarRocksMaterializedViewPreviewSql } from './tableDesignerSchemaSql';
|
||||
import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol';
|
||||
import { resolveConnectionHostTokens } from '../utils/tabDisplay';
|
||||
import {
|
||||
@@ -74,7 +75,7 @@ interface TreeNode {
|
||||
children?: TreeNode[];
|
||||
icon?: React.ReactNode;
|
||||
dataRef?: any;
|
||||
type?: 'connection' | 'database' | 'table' | 'view' | 'db-trigger' | 'routine' | 'object-group' | 'queries-folder' | 'saved-query' | 'external-sql-root' | 'external-sql-directory' | 'external-sql-folder' | 'external-sql-file' | 'folder-columns' | 'folder-indexes' | 'folder-fks' | 'folder-triggers' | 'redis-db' | 'tag' | 'jvm-mode' | 'jvm-resource' | 'jvm-diagnostic' | 'jvm-monitoring';
|
||||
type?: 'connection' | 'database' | 'table' | 'view' | 'materialized-view' | 'db-trigger' | 'routine' | 'object-group' | 'queries-folder' | 'saved-query' | 'external-sql-root' | 'external-sql-directory' | 'external-sql-folder' | 'external-sql-file' | 'folder-columns' | 'folder-indexes' | 'folder-fks' | 'folder-triggers' | 'redis-db' | 'tag' | 'jvm-mode' | 'jvm-resource' | 'jvm-diagnostic' | 'jvm-monitoring';
|
||||
}
|
||||
|
||||
export const resolveSidebarTableNameForCopy = (node: Pick<TreeNode, 'title' | 'dataRef'> | null | undefined): string => {
|
||||
@@ -841,7 +842,8 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const buildViewsMetadataQuerySpecs = (dialect: string, dbName: string): MetadataQuerySpec[] => {
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'mysql': {
|
||||
case 'mysql':
|
||||
case 'starrocks': {
|
||||
const dbIdent = String(dbName || '').replace(/`/g, '``').trim();
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{
|
||||
@@ -886,7 +888,8 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const buildTriggersMetadataQuerySpecs = (dialect: string, dbName: string): MetadataQuerySpec[] => {
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'mysql': {
|
||||
case 'mysql':
|
||||
case 'starrocks': {
|
||||
const dbIdent = String(dbName || '').replace(/`/g, '``').trim();
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{
|
||||
@@ -927,6 +930,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks':
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{
|
||||
sql: safeDbName
|
||||
@@ -1047,6 +1051,46 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
return { views, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadStarRocksMaterializedViews = async (
|
||||
conn: any,
|
||||
dbName: string
|
||||
): Promise<{ views: string[]; supported: boolean }> => {
|
||||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||||
if (dialect !== 'starrocks') {
|
||||
return { views: [], supported: false };
|
||||
}
|
||||
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
const dbIdent = String(dbName || '').replace(/`/g, '``').trim();
|
||||
const querySpecs = normalizeMetadataQuerySpecs([
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT TABLE_SCHEMA AS schema_name, TABLE_NAME AS object_name FROM information_schema.tables WHERE TABLE_SCHEMA = '${safeDbName}' AND UPPER(TABLE_TYPE) LIKE '%MATERIALIZED%' ORDER BY TABLE_NAME`
|
||||
: '',
|
||||
},
|
||||
{ sql: dbIdent ? `SHOW MATERIALIZED VIEWS FROM \`${dbIdent}\`` : '' },
|
||||
{ sql: `SHOW MATERIALIZED VIEWS` },
|
||||
]);
|
||||
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(conn, dbName, querySpecs);
|
||||
const seen = new Set<string>();
|
||||
const views: string[] = [];
|
||||
|
||||
results.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const schemaName = getCaseInsensitiveValue(row, ['schema_name', 'table_schema', 'db', 'database']);
|
||||
const viewName =
|
||||
getCaseInsensitiveValue(row, ['object_name', 'view_name', 'table_name', 'name', 'materialized_view_name', 'mv_name'])
|
||||
|| getFirstRowValue(row);
|
||||
const fullName = normalizeSidebarViewName(dialect, dbName, schemaName, viewName);
|
||||
if (!fullName || seen.has(fullName)) return;
|
||||
seen.add(fullName);
|
||||
views.push(fullName);
|
||||
});
|
||||
});
|
||||
|
||||
return { views, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadDatabaseTriggers = async (
|
||||
conn: any,
|
||||
dbName: string
|
||||
@@ -1425,8 +1469,9 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
};
|
||||
});
|
||||
|
||||
const [viewsResult, triggersResult, routinesResult] = await Promise.all([
|
||||
const [viewsResult, materializedViewsResult, triggersResult, routinesResult] = await Promise.all([
|
||||
loadViews(conn, conn.dbName),
|
||||
loadStarRocksMaterializedViews(conn, conn.dbName),
|
||||
loadDatabaseTriggers(conn, conn.dbName),
|
||||
loadFunctions(conn, conn.dbName),
|
||||
]);
|
||||
@@ -1459,6 +1504,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
}));
|
||||
|
||||
const viewRows: string[] = Array.isArray(viewsResult.views) ? viewsResult.views : [];
|
||||
const materializedViewRows: string[] = Array.isArray(materializedViewsResult.views) ? materializedViewsResult.views : [];
|
||||
const triggerRows: any[] = Array.isArray(triggersResult.triggers) ? triggersResult.triggers : [];
|
||||
const routineRows: any[] = Array.isArray(routinesResult.routines) ? routinesResult.routines : [];
|
||||
|
||||
@@ -1471,6 +1517,15 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
};
|
||||
});
|
||||
|
||||
const materializedViewEntries = materializedViewRows.map((viewName: string) => {
|
||||
const parsed = splitQualifiedName(viewName);
|
||||
return {
|
||||
viewName,
|
||||
schemaName: parsed.schemaName,
|
||||
displayName: getSidebarTableDisplayName(conn, viewName),
|
||||
};
|
||||
});
|
||||
|
||||
const triggerEntries = (() => {
|
||||
const deduped: Array<{ displayName: string; triggerName: string; tableName: string; schemaName: string }> = [];
|
||||
const triggerSeen = new Set<string>();
|
||||
@@ -1548,6 +1603,8 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
// Sort views by name (case-insensitive)
|
||||
viewEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
|
||||
|
||||
materializedViewEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
|
||||
|
||||
// Sort triggers by display name (case-insensitive)
|
||||
triggerEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
|
||||
|
||||
@@ -1572,6 +1629,15 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
isLeaf: true,
|
||||
});
|
||||
|
||||
const buildMaterializedViewNode = (entry: { viewName: string; schemaName: string; displayName: string }): TreeNode => ({
|
||||
title: entry.displayName,
|
||||
key: `${conn.id}-${conn.dbName}-materialized-view-${entry.viewName}`,
|
||||
icon: <ThunderboltOutlined />,
|
||||
type: 'materialized-view',
|
||||
dataRef: { ...conn, viewName: entry.viewName, tableName: entry.viewName, schemaName: entry.schemaName, objectKind: 'materialized-view' },
|
||||
isLeaf: true,
|
||||
});
|
||||
|
||||
const buildTriggerNode = (entry: { triggerName: string; tableName: string; schemaName: string; displayName: string }): TreeNode => ({
|
||||
title: entry.displayName,
|
||||
key: `${conn.id}-${conn.dbName}-trigger-${entry.triggerName}-${entry.tableName}`,
|
||||
@@ -1613,6 +1679,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
schemaName: string;
|
||||
tables: TreeNode[];
|
||||
views: TreeNode[];
|
||||
materializedViews: TreeNode[];
|
||||
routines: TreeNode[];
|
||||
triggers: TreeNode[];
|
||||
};
|
||||
@@ -1627,6 +1694,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
schemaName,
|
||||
tables: [],
|
||||
views: [],
|
||||
materializedViews: [],
|
||||
routines: [],
|
||||
triggers: [],
|
||||
};
|
||||
@@ -1637,11 +1705,13 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
|
||||
tableEntries.forEach((entry) => getSchemaBucket(entry.schemaName).tables.push(buildTableNode(entry)));
|
||||
viewEntries.forEach((entry) => getSchemaBucket(entry.schemaName).views.push(buildViewNode(entry)));
|
||||
materializedViewEntries.forEach((entry) => getSchemaBucket(entry.schemaName).materializedViews.push(buildMaterializedViewNode(entry)));
|
||||
routineEntries.forEach((entry) => getSchemaBucket(entry.schemaName).routines.push(buildRoutineNode(entry)));
|
||||
triggerEntries.forEach((entry) => getSchemaBucket(entry.schemaName).triggers.push(buildTriggerNode(entry)));
|
||||
|
||||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||||
const isOracleLike = (dialect === 'oracle' || dialect === 'dm');
|
||||
const includeMaterializedViews = dialect === 'starrocks';
|
||||
|
||||
const schemaNodes: TreeNode[] = Array.from(schemaMap.values())
|
||||
.filter((bucket) => !(isOracleLike && !bucket.schemaName))
|
||||
@@ -1657,6 +1727,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const groupedNodes: TreeNode[] = [
|
||||
buildObjectGroup(schemaNodeKey, 'tables', '表', <TableOutlined />, bucket.tables, { schemaName: bucket.schemaName }),
|
||||
buildObjectGroup(schemaNodeKey, 'views', '视图', <EyeOutlined />, bucket.views, { schemaName: bucket.schemaName }),
|
||||
...(includeMaterializedViews ? [buildObjectGroup(schemaNodeKey, 'materializedViews', '物化视图', <ThunderboltOutlined />, bucket.materializedViews, { schemaName: bucket.schemaName })] : []),
|
||||
buildObjectGroup(schemaNodeKey, 'routines', '函数', <CodeOutlined />, bucket.routines, { schemaName: bucket.schemaName }),
|
||||
buildObjectGroup(schemaNodeKey, 'triggers', '触发器', <FunctionOutlined />, bucket.triggers, { schemaName: bucket.schemaName }),
|
||||
];
|
||||
@@ -1674,9 +1745,11 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
|
||||
replaceTreeNodeChildren(key, [queriesNode, externalSQLRootNode, ...schemaNodes]);
|
||||
} else {
|
||||
const includeMaterializedViews = getMetadataDialect(conn as SavedConnection) === 'starrocks';
|
||||
const groupedNodes: TreeNode[] = [
|
||||
buildObjectGroup(key as string, 'tables', '表', <TableOutlined />, tableEntries.map(buildTableNode)),
|
||||
buildObjectGroup(key as string, 'views', '视图', <EyeOutlined />, viewEntries.map(buildViewNode)),
|
||||
...(includeMaterializedViews ? [buildObjectGroup(key as string, 'materializedViews', '物化视图', <ThunderboltOutlined />, materializedViewEntries.map(buildMaterializedViewNode))] : []),
|
||||
buildObjectGroup(key as string, 'routines', '函数', <CodeOutlined />, routineEntries.map(buildRoutineNode)),
|
||||
buildObjectGroup(key as string, 'triggers', '触发器', <FunctionOutlined />, triggerEntries.map(buildTriggerNode)),
|
||||
];
|
||||
@@ -1719,7 +1792,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const target = resolveSidebarLocateTarget(request, {
|
||||
groupBySchema: shouldHideSchemaPrefix(conn),
|
||||
});
|
||||
const objectLabel = request.objectGroup === 'views' ? '视图' : '表';
|
||||
const objectLabel = request.objectGroup === 'materializedViews' ? '物化视图' : (request.objectGroup === 'views' ? '视图' : '表');
|
||||
|
||||
let path = findSidebarNodePathForLocate(treeDataRef.current as SidebarLocateTreeNodeLike[], target);
|
||||
const dbLoadKey = `dbs-${request.connectionId}`;
|
||||
@@ -1893,7 +1966,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
setActiveContext({ connectionId: dataRef.id, dbName: dataRef.dbName });
|
||||
} else if (type === 'jvm-mode' || type === 'jvm-resource' || type === 'jvm-diagnostic' || type === 'jvm-monitoring') {
|
||||
setActiveContext({ connectionId: dataRef.id, dbName: '' });
|
||||
} else if (type === 'view' || type === 'db-trigger' || type === 'routine') {
|
||||
} else if (type === 'view' || type === 'materialized-view' || type === 'db-trigger' || type === 'routine') {
|
||||
setActiveContext({ connectionId: dataRef.id, dbName: dataRef.dbName });
|
||||
} else if (type === 'saved-query') {
|
||||
setActiveContext({ connectionId: dataRef.connectionId, dbName: dataRef.dbName });
|
||||
@@ -1940,7 +2013,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
if (type === 'connection') setActiveContext({ connectionId: nodeKey, dbName: '' });
|
||||
else if (type === 'database') setActiveContext({ connectionId: dataRef.id, dbName: dataRef.dbName });
|
||||
else if (type === 'jvm-mode' || type === 'jvm-resource' || type === 'jvm-diagnostic' || type === 'jvm-monitoring') setActiveContext({ connectionId: dataRef.id, dbName: '' });
|
||||
else if (type === 'table' || type === 'view' || type === 'db-trigger' || type === 'routine') setActiveContext({ connectionId: dataRef.id, dbName: dataRef.dbName });
|
||||
else if (type === 'table' || type === 'view' || type === 'materialized-view' || type === 'db-trigger' || type === 'routine') setActiveContext({ connectionId: dataRef.id, dbName: dataRef.dbName });
|
||||
else if (type === 'saved-query') setActiveContext({ connectionId: dataRef.connectionId, dbName: dataRef.dbName });
|
||||
else if (type === 'external-sql-root' || type === 'external-sql-directory' || type === 'external-sql-folder' || type === 'external-sql-file') setActiveContext({ connectionId: dataRef.connectionId, dbName: dataRef.dbName });
|
||||
else if (type === 'redis-db') setActiveContext({ connectionId: dataRef.id, dbName: `db${dataRef.redisDB}` });
|
||||
@@ -1958,7 +2031,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
tableName,
|
||||
});
|
||||
return;
|
||||
} else if (node.type === 'view') {
|
||||
} else if (node.type === 'view' || node.type === 'materialized-view') {
|
||||
const { viewName, dbName, id } = node.dataRef;
|
||||
addTab({
|
||||
id: node.key,
|
||||
@@ -2145,7 +2218,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
if (node.type === 'database') {
|
||||
connId = node.dataRef.id;
|
||||
dbName = node.title;
|
||||
} else if (node.type === 'table' || node.type === 'view') {
|
||||
} else if (node.type === 'table' || node.type === 'view' || node.type === 'materialized-view') {
|
||||
connId = node.dataRef.id;
|
||||
dbName = node.dataRef.dbName;
|
||||
}
|
||||
@@ -3138,13 +3211,15 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
// --- 视图操作 ---
|
||||
const openViewDefinition = (node: any) => {
|
||||
const { viewName, dbName, id } = node.dataRef;
|
||||
const isMaterialized = node.type === 'materialized-view' || node.dataRef?.objectKind === 'materialized-view';
|
||||
addTab({
|
||||
id: `view-def-${id}-${dbName}-${viewName}`,
|
||||
title: `视图: ${viewName}`,
|
||||
title: `${isMaterialized ? '物化视图' : '视图'}: ${viewName}`,
|
||||
type: 'view-def',
|
||||
connectionId: id,
|
||||
dbName,
|
||||
viewName,
|
||||
viewKind: isMaterialized ? 'materialized' : 'view',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3160,6 +3235,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
let query = '';
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks':
|
||||
query = `SHOW CREATE VIEW \`${viewName.replace(/`/g, '``')}\``;
|
||||
break;
|
||||
case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': case 'opengauss': {
|
||||
@@ -3216,6 +3292,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
let template: string;
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks':
|
||||
template = `CREATE VIEW \`view_name\` AS\nSELECT column1, column2\nFROM table_name\nWHERE condition;`;
|
||||
break;
|
||||
case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': case 'opengauss':
|
||||
@@ -3244,6 +3321,56 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
});
|
||||
};
|
||||
|
||||
const openCreateStarRocksMaterializedView = (node: any) => {
|
||||
const conn = node.dataRef;
|
||||
const { dbName, id } = conn;
|
||||
const schemaPrefix = String(conn.schemaName || dbName || '').trim();
|
||||
const mvName = schemaPrefix ? `${schemaPrefix}.mv_name` : 'mv_name';
|
||||
const template = buildStarRocksMaterializedViewPreviewSql({
|
||||
name: mvName,
|
||||
query: 'SELECT\n column1,\n COUNT(*) AS cnt\nFROM table_name\nGROUP BY column1',
|
||||
distributionColumnNames: ['column1'],
|
||||
refreshClause: 'REFRESH ASYNC',
|
||||
properties: '"replication_num" = "1"',
|
||||
});
|
||||
addTab({
|
||||
id: `query-create-starrocks-mv-${Date.now()}`,
|
||||
title: '新建物化视图',
|
||||
type: 'query',
|
||||
connectionId: id,
|
||||
dbName,
|
||||
query: template,
|
||||
});
|
||||
};
|
||||
|
||||
const openCreateStarRocksExternalCatalog = (node: any) => {
|
||||
const conn = node.dataRef;
|
||||
const { dbName, id } = conn;
|
||||
addTab({
|
||||
id: `query-create-starrocks-catalog-${Date.now()}`,
|
||||
title: '新建外部 Catalog',
|
||||
type: 'query',
|
||||
connectionId: id,
|
||||
dbName,
|
||||
query: `CREATE EXTERNAL CATALOG catalog_name\nPROPERTIES (\n "type" = "hive",\n "hive.metastore.uris" = "thrift://127.0.0.1:9083"\n);`,
|
||||
});
|
||||
};
|
||||
|
||||
const openCreateStarRocksRollup = (node: any) => {
|
||||
const conn = node.dataRef;
|
||||
const { tableName, dbName, id } = conn;
|
||||
const safeTable = String(tableName || 'table_name').trim();
|
||||
const quotedTable = safeTable.includes('`') ? safeTable : safeTable.split('.').map(part => `\`${part.replace(/`/g, '``')}\``).join('.');
|
||||
addTab({
|
||||
id: `query-create-starrocks-rollup-${Date.now()}`,
|
||||
title: '新增 Rollup',
|
||||
type: 'query',
|
||||
connectionId: id,
|
||||
dbName,
|
||||
query: `ALTER TABLE ${quotedTable}\nADD ROLLUP rollup_name (column1, column2);`,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDropView = (node: any) => {
|
||||
const conn = node.dataRef;
|
||||
const viewName = String(conn.viewName || '').trim();
|
||||
@@ -3327,6 +3454,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks':
|
||||
query = `SHOW CREATE ${routineType} \`${name.replace(/`/g, '``')}\``;
|
||||
break;
|
||||
case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': case 'opengauss': {
|
||||
@@ -3395,6 +3523,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks':
|
||||
template = isProc
|
||||
? `DELIMITER $$\nCREATE PROCEDURE proc_name(IN param1 INT)\nBEGIN\n SELECT * FROM table_name WHERE id = param1;\nEND$$\nDELIMITER ;`
|
||||
: `DELIMITER $$\nCREATE FUNCTION func_name(param1 INT)\nRETURNS INT\nDETERMINISTIC\nBEGIN\n RETURN param1 * 2;\nEND$$\nDELIMITER ;`;
|
||||
@@ -3626,6 +3755,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const isObjectNode = (node: TreeNode): boolean => {
|
||||
return node.type === 'table'
|
||||
|| node.type === 'view'
|
||||
|| node.type === 'materialized-view'
|
||||
|| node.type === 'db-trigger'
|
||||
|| node.type === 'routine'
|
||||
|| node.type === 'object-group';
|
||||
@@ -3740,6 +3870,17 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
];
|
||||
}
|
||||
|
||||
if (node.type === 'object-group' && node.dataRef?.groupKey === 'materializedViews') {
|
||||
return [
|
||||
{
|
||||
key: 'create-materialized-view',
|
||||
label: '新建物化视图',
|
||||
icon: <PlusOutlined />,
|
||||
onClick: () => openCreateStarRocksMaterializedView(node)
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// 函数分组节点的右键菜单
|
||||
if (node.type === 'object-group' && node.dataRef?.groupKey === 'routines') {
|
||||
const dialect = getMetadataDialect(node.dataRef as SavedConnection);
|
||||
@@ -4107,6 +4248,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
}
|
||||
];
|
||||
} else if (node.type === 'database') {
|
||||
const isStarRocks = getMetadataDialect(node.dataRef as SavedConnection) === 'starrocks';
|
||||
return [
|
||||
{
|
||||
key: 'new-table',
|
||||
@@ -4114,6 +4256,20 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
icon: <TableOutlined />,
|
||||
onClick: () => openNewTableDesign(node)
|
||||
},
|
||||
...(isStarRocks ? [
|
||||
{
|
||||
key: 'new-materialized-view',
|
||||
label: '新建物化视图',
|
||||
icon: <ThunderboltOutlined />,
|
||||
onClick: () => openCreateStarRocksMaterializedView(node)
|
||||
},
|
||||
{
|
||||
key: 'new-external-catalog',
|
||||
label: '新建外部 Catalog',
|
||||
icon: <CloudOutlined />,
|
||||
onClick: () => openCreateStarRocksExternalCatalog(node)
|
||||
},
|
||||
] : []),
|
||||
{
|
||||
key: 'rename-db',
|
||||
label: '重命名数据库',
|
||||
@@ -4263,6 +4419,36 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
]
|
||||
},
|
||||
];
|
||||
} else if (node.type === 'materialized-view') {
|
||||
return [
|
||||
{
|
||||
key: 'open-materialized-view',
|
||||
label: '浏览物化视图数据',
|
||||
icon: <EyeOutlined />,
|
||||
onClick: () => onDoubleClick(null, node)
|
||||
},
|
||||
{
|
||||
key: 'materialized-view-definition',
|
||||
label: '查看物化视图定义',
|
||||
icon: <CodeOutlined />,
|
||||
onClick: () => openViewDefinition(node)
|
||||
},
|
||||
{
|
||||
key: 'new-query',
|
||||
label: '新建查询',
|
||||
icon: <ConsoleSqlOutlined />,
|
||||
onClick: () => {
|
||||
addTab({
|
||||
id: `query-${Date.now()}`,
|
||||
title: `新建查询`,
|
||||
type: 'query',
|
||||
connectionId: node.dataRef.id,
|
||||
dbName: node.dataRef.dbName,
|
||||
query: buildTableSelectQuery('starrocks', String(node.dataRef?.tableName || node.dataRef?.viewName || ''))
|
||||
});
|
||||
}
|
||||
},
|
||||
];
|
||||
} else if (node.type === 'routine') {
|
||||
const routineType = node.dataRef?.routineType || 'FUNCTION';
|
||||
const typeLabel = routineType === 'PROCEDURE' ? '存储过程' : '函数';
|
||||
@@ -4296,6 +4482,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
},
|
||||
];
|
||||
} else if (node.type === 'table') {
|
||||
const isStarRocks = getMetadataDialect(node.dataRef as SavedConnection) === 'starrocks';
|
||||
return [
|
||||
{
|
||||
key: 'new-query',
|
||||
@@ -4321,6 +4508,12 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
icon: <EditOutlined />,
|
||||
onClick: () => openDesign(node, 'columns', false)
|
||||
},
|
||||
...(isStarRocks ? [{
|
||||
key: 'new-rollup',
|
||||
label: '新增 Rollup',
|
||||
icon: <ThunderboltOutlined />,
|
||||
onClick: () => openCreateStarRocksRollup(node)
|
||||
}] : []),
|
||||
{
|
||||
key: 'copy-table-name',
|
||||
label: '复制表名',
|
||||
@@ -4507,7 +4700,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
|
||||
const displayTitle = String(node.title ?? '');
|
||||
let hoverTitle = displayTitle;
|
||||
if (node.type === 'table' || node.type === 'view') {
|
||||
if (node.type === 'table' || node.type === 'view' || node.type === 'materialized-view') {
|
||||
const rawTableName = String(node?.dataRef?.tableName || node?.dataRef?.viewName || '').trim();
|
||||
const conn = node?.dataRef as SavedConnection | undefined;
|
||||
if (rawTableName && shouldHideSchemaPrefix(conn)) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { TabData, ColumnDefinition, IndexDefinition, ForeignKeyDefinition, Trigg
|
||||
import { useStore } from '../store';
|
||||
import { DBGetColumns, DBGetIndexes, DBQuery, DBGetForeignKeys, DBGetTriggers, DBShowCreateTable } from '../../wailsjs/go/app/App';
|
||||
import { hasIndexFormChanged, normalizeIndexFormFromRow, shouldRestoreOriginalIndex, toggleIndexSelection as getNextIndexSelection, type IndexDisplaySnapshot } from './tableDesignerIndexUtils';
|
||||
import { buildAlterTablePreviewSql, buildCreateTablePreviewSql, hasAlterTableDraftChanges } from './tableDesignerSchemaSql';
|
||||
import { buildAlterTablePreviewSql, buildCreateTablePreviewSql, hasAlterTableDraftChanges, type StarRocksCreateTableOptions, type StarRocksDistributionType, type StarRocksKeyModel, type StarRocksTableKind } from './tableDesignerSchemaSql';
|
||||
import TableDesignerSqlPreview from './TableDesignerSqlPreview';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { noAutoCapInputProps } from '../utils/inputAutoCap';
|
||||
@@ -367,6 +367,18 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
const [newTableName, setNewTableName] = useState('');
|
||||
const [charset, setCharset] = useState('utf8mb4');
|
||||
const [collation, setCollation] = useState('utf8mb4_unicode_ci');
|
||||
const [starRocksTableKind, setStarRocksTableKind] = useState<StarRocksTableKind>('olap');
|
||||
const [starRocksKeyModel, setStarRocksKeyModel] = useState<StarRocksKeyModel>('DUPLICATE');
|
||||
const [starRocksKeyColumns, setStarRocksKeyColumns] = useState<string[]>([]);
|
||||
const [starRocksPartitionClause, setStarRocksPartitionClause] = useState('');
|
||||
const [starRocksDistributionType, setStarRocksDistributionType] = useState<StarRocksDistributionType>('HASH');
|
||||
const [starRocksDistributionColumns, setStarRocksDistributionColumns] = useState<string[]>([]);
|
||||
const [starRocksBucketMode, setStarRocksBucketMode] = useState<'AUTO' | 'NUMBER'>('AUTO');
|
||||
const [starRocksBucketCount, setStarRocksBucketCount] = useState('');
|
||||
const [starRocksProperties, setStarRocksProperties] = useState('');
|
||||
const [starRocksRollups, setStarRocksRollups] = useState('');
|
||||
const [starRocksExternalEngine, setStarRocksExternalEngine] = useState('hive');
|
||||
const [starRocksExternalProperties, setStarRocksExternalProperties] = useState('"resource" = "hive0"\n"database" = "raw_db"\n"table" = "raw_table"');
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [previewSql, setPreviewSql] = useState<string>('');
|
||||
@@ -849,11 +861,11 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
|| customDriver === 'sphinx'
|
||||
|| customDriver === 'tidb'
|
||||
|| customDriver === 'oceanbase'
|
||||
|| customDriver === 'starrocks'
|
||||
|| customDriver.includes('mysql')
|
||||
) {
|
||||
return 'mysql';
|
||||
}
|
||||
if (customDriver === 'starrocks') return 'starrocks';
|
||||
if (customDriver === 'dameng') return 'dm';
|
||||
return customDriver;
|
||||
};
|
||||
@@ -876,6 +888,7 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
case 'mariadb':
|
||||
case 'oceanbase':
|
||||
case 'diros':
|
||||
case 'starrocks':
|
||||
return `CREATE TRIGGER trigger_name
|
||||
BEFORE INSERT ON \`${tblName}\`
|
||||
FOR EACH ROW
|
||||
@@ -938,6 +951,7 @@ END;`;
|
||||
case 'mariadb':
|
||||
case 'oceanbase':
|
||||
case 'diros':
|
||||
case 'starrocks':
|
||||
return `DROP TRIGGER IF EXISTS \`${triggerName}\``;
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
@@ -1309,6 +1323,43 @@ ${selectedTrigger.statement}`;
|
||||
[columns]
|
||||
);
|
||||
|
||||
const isStarRocksNewTable = isNewTable && getDbType() === 'starrocks';
|
||||
|
||||
const parseStarRocksRollupOptions = (raw: string): StarRocksCreateTableOptions['rollups'] => (
|
||||
String(raw || '')
|
||||
.split(/\r?\n/)
|
||||
.map(line => line.trim())
|
||||
.filter(Boolean)
|
||||
.map(line => {
|
||||
const [namePart, columnsPart] = line.split(':');
|
||||
const name = String(namePart || '').trim();
|
||||
const columnNames = String(columnsPart || '')
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
return { name, columnNames };
|
||||
})
|
||||
.filter(item => item.name && item.columnNames.length > 0)
|
||||
);
|
||||
|
||||
const buildStarRocksCreateOptions = (): StarRocksCreateTableOptions | undefined => {
|
||||
if (!isStarRocksNewTable) return undefined;
|
||||
return {
|
||||
tableKind: starRocksTableKind,
|
||||
keyModel: starRocksKeyModel,
|
||||
keyColumnNames: starRocksKeyColumns,
|
||||
partitionClause: starRocksPartitionClause,
|
||||
distributionType: starRocksDistributionType,
|
||||
distributionColumnNames: starRocksDistributionColumns,
|
||||
bucketMode: starRocksBucketMode,
|
||||
bucketCount: Number(starRocksBucketCount) || undefined,
|
||||
properties: starRocksProperties,
|
||||
rollups: parseStarRocksRollupOptions(starRocksRollups),
|
||||
externalEngine: starRocksExternalEngine,
|
||||
externalProperties: starRocksExternalProperties,
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedIndexKeys.length === 0) return;
|
||||
const validKeys = selectedIndexKeys.filter(key => groupedIndexes.some(idx => idx.key === key));
|
||||
@@ -1489,6 +1540,7 @@ ${selectedTrigger.statement}`;
|
||||
columns: targetColumns,
|
||||
charset: targetCharset,
|
||||
collation: targetCollation,
|
||||
starRocksOptions: buildStarRocksCreateOptions(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2350,6 +2402,134 @@ END;`;
|
||||
})),
|
||||
];
|
||||
|
||||
const starRocksAdvancedTabContent = (
|
||||
<div style={{ height: '100%', overflow: 'auto', padding: 12 }}>
|
||||
<Space direction="vertical" size={14} style={{ width: '100%', maxWidth: 960 }}>
|
||||
<Radio.Group
|
||||
value={starRocksTableKind}
|
||||
onChange={(e) => setStarRocksTableKind(e.target.value)}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
options={[
|
||||
{ label: 'OLAP 表', value: 'olap' },
|
||||
{ label: '外部表', value: 'external' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{starRocksTableKind === 'olap' ? (
|
||||
<>
|
||||
<Space wrap>
|
||||
<Select
|
||||
value={starRocksKeyModel}
|
||||
onChange={setStarRocksKeyModel}
|
||||
options={[
|
||||
{ label: 'Duplicate Key', value: 'DUPLICATE' },
|
||||
{ label: 'Primary Key', value: 'PRIMARY' },
|
||||
{ label: 'Unique Key', value: 'UNIQUE' },
|
||||
{ label: 'Aggregate Key', value: 'AGGREGATE' },
|
||||
]}
|
||||
style={{ width: 180 }}
|
||||
/>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
placeholder="Key 字段"
|
||||
value={starRocksKeyColumns}
|
||||
onChange={setStarRocksKeyColumns}
|
||||
options={localColumnOptions}
|
||||
style={{ minWidth: 280 }}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
<Input.TextArea
|
||||
value={starRocksPartitionClause}
|
||||
onChange={(e) => setStarRocksPartitionClause(e.target.value)}
|
||||
autoSize={{ minRows: 3, maxRows: 8 }}
|
||||
placeholder={'PARTITION BY date_trunc(\'day\', `event_time`)\n-- 或按业务需要填写完整 PARTITION BY 子句'}
|
||||
/>
|
||||
|
||||
<Space wrap>
|
||||
<Select
|
||||
value={starRocksDistributionType}
|
||||
onChange={setStarRocksDistributionType}
|
||||
options={[
|
||||
{ label: 'Hash 分桶', value: 'HASH' },
|
||||
{ label: 'Random 分桶', value: 'RANDOM' },
|
||||
{ label: '不生成分桶子句', value: 'NONE' },
|
||||
]}
|
||||
style={{ width: 180 }}
|
||||
/>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
disabled={starRocksDistributionType !== 'HASH'}
|
||||
placeholder="分桶字段"
|
||||
value={starRocksDistributionColumns}
|
||||
onChange={setStarRocksDistributionColumns}
|
||||
options={localColumnOptions}
|
||||
style={{ minWidth: 260 }}
|
||||
/>
|
||||
<Select
|
||||
value={starRocksBucketMode}
|
||||
onChange={setStarRocksBucketMode}
|
||||
options={[
|
||||
{ label: 'Buckets Auto', value: 'AUTO' },
|
||||
{ label: '指定 Buckets', value: 'NUMBER' },
|
||||
]}
|
||||
style={{ width: 160 }}
|
||||
/>
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
disabled={starRocksBucketMode !== 'NUMBER'}
|
||||
value={starRocksBucketCount}
|
||||
onChange={(e) => setStarRocksBucketCount(e.target.value.replace(/[^\d]/g, ''))}
|
||||
placeholder="Buckets"
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
<Input.TextArea
|
||||
value={starRocksProperties}
|
||||
onChange={(e) => setStarRocksProperties(e.target.value)}
|
||||
autoSize={{ minRows: 3, maxRows: 8 }}
|
||||
placeholder={'"replication_num" = "1"\n"storage_medium" = "SSD"'}
|
||||
/>
|
||||
|
||||
<Input.TextArea
|
||||
value={starRocksRollups}
|
||||
onChange={(e) => setStarRocksRollups(e.target.value)}
|
||||
autoSize={{ minRows: 3, maxRows: 8 }}
|
||||
placeholder={'rollup_name: column1, column2\nrollup_daily: dt, user_id'}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Space wrap>
|
||||
<Select
|
||||
value={starRocksExternalEngine}
|
||||
onChange={setStarRocksExternalEngine}
|
||||
options={[
|
||||
{ label: 'Hive', value: 'hive' },
|
||||
{ label: 'MySQL', value: 'mysql' },
|
||||
{ label: 'Iceberg', value: 'iceberg' },
|
||||
{ label: 'Hudi', value: 'hudi' },
|
||||
{ label: 'JDBC', value: 'jdbc' },
|
||||
]}
|
||||
style={{ width: 180 }}
|
||||
/>
|
||||
</Space>
|
||||
<Input.TextArea
|
||||
value={starRocksExternalProperties}
|
||||
onChange={(e) => setStarRocksExternalProperties(e.target.value)}
|
||||
autoSize={{ minRows: 6, maxRows: 14 }}
|
||||
placeholder={'"resource" = "hive0"\n"database" = "raw_db"\n"table" = "raw_table"'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
|
||||
const columnsTabContent = (
|
||||
<div
|
||||
ref={containerRef}
|
||||
@@ -2593,6 +2773,13 @@ END;`;
|
||||
label: '字段',
|
||||
children: columnsTabContent
|
||||
},
|
||||
...(isStarRocksNewTable ? [
|
||||
{
|
||||
key: 'starrocks',
|
||||
label: 'StarRocks',
|
||||
children: starRocksAdvancedTabContent,
|
||||
},
|
||||
] : []),
|
||||
...(!isNewTable ? [
|
||||
{
|
||||
key: 'indexes',
|
||||
|
||||
@@ -72,6 +72,7 @@ const buildTableStatusSQL = (dialect: string, dbName: string, schemaName?: strin
|
||||
const escapeLiteral = (s: string) => s.replace(/'/g, "''");
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks':
|
||||
return `
|
||||
SELECT
|
||||
TABLE_NAME AS table_name,
|
||||
|
||||
@@ -53,6 +53,7 @@ const TriggerViewer: React.FC<TriggerViewerProps> = ({ tab }) => {
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks':
|
||||
return [
|
||||
`SHOW CREATE TRIGGER \`${triggerName.replace(/`/g, '``')}\``,
|
||||
safeDbName
|
||||
@@ -161,7 +162,8 @@ LIMIT 1`];
|
||||
const row = data[0];
|
||||
|
||||
switch (dialect) {
|
||||
case 'mysql': {
|
||||
case 'mysql':
|
||||
case 'starrocks': {
|
||||
// MySQL SHOW CREATE TRIGGER returns: Trigger, sql_mode, SQL Original Statement, ...
|
||||
const keys = Object.keys(row);
|
||||
if (row.trigger_definition || row.TRIGGER_DEFINITION) {
|
||||
|
||||
@@ -25,6 +25,8 @@ const resolveCustomDriverDialect = (driver: string): string => {
|
||||
case 'diros':
|
||||
case 'doris':
|
||||
return 'diros';
|
||||
case 'starrocks':
|
||||
return 'starrocks';
|
||||
case 'oceanbase':
|
||||
return 'oceanbase';
|
||||
case 'kingbase':
|
||||
@@ -49,6 +51,7 @@ const resolveCustomDriverDialect = (driver: string): string => {
|
||||
if (normalized.includes('sqlite')) return 'sqlite';
|
||||
if (normalized.includes('sphinx')) return 'sphinx';
|
||||
if (normalized.includes('diros') || normalized.includes('doris')) return 'diros';
|
||||
if (normalized.includes('starrocks')) return 'starrocks';
|
||||
return normalized;
|
||||
};
|
||||
|
||||
@@ -65,6 +68,7 @@ export const supportsTableTruncateAction = (type: string, driver?: string): bool
|
||||
case 'mysql':
|
||||
case 'mariadb':
|
||||
case 'oceanbase':
|
||||
case 'starrocks':
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
case 'highgo':
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
buildCreateTablePreviewSql,
|
||||
buildAlterTablePreviewSql,
|
||||
buildStarRocksMaterializedViewPreviewSql,
|
||||
hasAlterTableDraftChanges,
|
||||
type BuildAlterTablePreviewInput,
|
||||
type EditableColumnSnapshot,
|
||||
@@ -218,6 +219,88 @@ describe('tableDesignerSchemaSql', () => {
|
||||
expect(tdengineSql).not.toContain('AFTER');
|
||||
});
|
||||
|
||||
it('builds StarRocks create table preview with OLAP engine and conservative distribution', () => {
|
||||
const sql = buildCreateTablePreviewSql({
|
||||
tableName: 'sales.orders',
|
||||
dbType: 'starrocks',
|
||||
columns: [
|
||||
baseColumn({ _key: 'id', name: 'id', type: 'BIGINT', nullable: 'NO', key: 'PRI' }),
|
||||
baseColumn({ _key: 'amount', name: 'amount', type: 'DECIMAL(10,2)', nullable: 'YES' }),
|
||||
],
|
||||
});
|
||||
|
||||
expect(sql).toContain('CREATE TABLE `sales`.`orders`');
|
||||
expect(sql).toContain('ENGINE=OLAP');
|
||||
expect(sql).toContain('DUPLICATE KEY (`id`)');
|
||||
expect(sql).toContain('DISTRIBUTED BY HASH(`id`) BUCKETS AUTO');
|
||||
expect(sql).not.toContain('ENGINE=InnoDB');
|
||||
});
|
||||
|
||||
it('builds StarRocks advanced OLAP table preview with key model, partition, buckets, properties and rollup', () => {
|
||||
const sql = buildCreateTablePreviewSql({
|
||||
tableName: 'sales.events',
|
||||
dbType: 'starrocks',
|
||||
columns: [
|
||||
baseColumn({ _key: 'dt', name: 'dt', type: 'DATE', nullable: 'NO' }),
|
||||
baseColumn({ _key: 'user_id', name: 'user_id', type: 'BIGINT', nullable: 'NO' }),
|
||||
baseColumn({ _key: 'amount', name: 'amount', type: 'DECIMAL(10,2)', nullable: 'YES', extra: 'SUM' }),
|
||||
],
|
||||
starRocksOptions: {
|
||||
keyModel: 'AGGREGATE',
|
||||
keyColumnNames: ['dt', 'user_id'],
|
||||
partitionClause: 'PARTITION BY date_trunc(\'day\', `dt`)',
|
||||
distributionColumnNames: ['user_id'],
|
||||
bucketMode: 'NUMBER',
|
||||
bucketCount: 12,
|
||||
properties: '"replication_num" = "1"',
|
||||
rollups: [{ name: 'rollup_dt', columnNames: ['dt', 'amount'] }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(sql).toContain('AGGREGATE KEY (`dt`, `user_id`)');
|
||||
expect(sql).toContain("PARTITION BY date_trunc('day', `dt`)");
|
||||
expect(sql).toContain('DISTRIBUTED BY HASH(`user_id`) BUCKETS 12');
|
||||
expect(sql).toContain('PROPERTIES (');
|
||||
expect(sql).toContain('ALTER TABLE `sales`.`events`\nADD ROLLUP `rollup_dt` (`dt`, `amount`);');
|
||||
});
|
||||
|
||||
it('builds StarRocks external table preview with external engine and properties', () => {
|
||||
const sql = buildCreateTablePreviewSql({
|
||||
tableName: 'ext.raw_orders',
|
||||
dbType: 'starrocks',
|
||||
columns: [
|
||||
baseColumn({ _key: 'id', name: 'id', type: 'BIGINT', nullable: 'NO' }),
|
||||
baseColumn({ _key: 'payload', name: 'payload', type: 'STRING', nullable: 'YES' }),
|
||||
],
|
||||
starRocksOptions: {
|
||||
tableKind: 'external',
|
||||
externalEngine: 'hive',
|
||||
externalProperties: '"resource" = "hive0"\n"database" = "ods"\n"table" = "orders"',
|
||||
},
|
||||
});
|
||||
|
||||
expect(sql).toContain('CREATE EXTERNAL TABLE `ext`.`raw_orders`');
|
||||
expect(sql).toContain('ENGINE=HIVE');
|
||||
expect(sql).toContain('"resource" = "hive0"');
|
||||
expect(sql).not.toContain('ENGINE=OLAP');
|
||||
});
|
||||
|
||||
it('builds StarRocks materialized view preview with refresh and distribution clauses', () => {
|
||||
const sql = buildStarRocksMaterializedViewPreviewSql({
|
||||
name: 'sales.mv_user_amount',
|
||||
query: 'SELECT user_id, SUM(amount) AS total_amount FROM sales.events GROUP BY user_id',
|
||||
distributionColumnNames: ['user_id'],
|
||||
bucketCount: 8,
|
||||
refreshClause: 'REFRESH SCHEDULE EVERY(INTERVAL 10 MINUTE)',
|
||||
properties: '"replication_num" = "1"',
|
||||
});
|
||||
|
||||
expect(sql).toContain('CREATE MATERIALIZED VIEW `sales`.`mv_user_amount`');
|
||||
expect(sql).toContain('REFRESH SCHEDULE EVERY(INTERVAL 10 MINUTE)');
|
||||
expect(sql).toContain('DISTRIBUTED BY HASH(`user_id`) BUCKETS 8');
|
||||
expect(sql).toContain('AS\nSELECT user_id, SUM(amount) AS total_amount FROM sales.events GROUP BY user_id;');
|
||||
});
|
||||
|
||||
it('treats mariadb and sphinx as mysql-family only where mysql syntax is intended', () => {
|
||||
for (const dbType of ['mariadb', 'sphinx']) {
|
||||
const sql = buildAlterTablePreviewSql(buildInput({ dbType }));
|
||||
|
||||
@@ -36,6 +36,46 @@ export interface BuildCreateTablePreviewInput {
|
||||
columns: EditableColumnSnapshot[];
|
||||
charset?: string;
|
||||
collation?: string;
|
||||
starRocksOptions?: StarRocksCreateTableOptions;
|
||||
}
|
||||
|
||||
export type StarRocksTableKind = 'olap' | 'external';
|
||||
export type StarRocksKeyModel = 'DUPLICATE' | 'PRIMARY' | 'UNIQUE' | 'AGGREGATE';
|
||||
export type StarRocksDistributionType = 'HASH' | 'RANDOM' | 'NONE';
|
||||
|
||||
export interface StarRocksRollupOption {
|
||||
name: string;
|
||||
columnNames: string[];
|
||||
fromIndexName?: string;
|
||||
properties?: string;
|
||||
}
|
||||
|
||||
export interface StarRocksCreateTableOptions {
|
||||
tableKind?: StarRocksTableKind;
|
||||
keyModel?: StarRocksKeyModel;
|
||||
keyColumnNames?: string[];
|
||||
partitionClause?: string;
|
||||
distributionType?: StarRocksDistributionType;
|
||||
distributionColumnNames?: string[];
|
||||
bucketMode?: 'AUTO' | 'NUMBER';
|
||||
bucketCount?: number;
|
||||
properties?: string;
|
||||
rollups?: StarRocksRollupOption[];
|
||||
externalEngine?: string;
|
||||
externalProperties?: string;
|
||||
}
|
||||
|
||||
export interface BuildStarRocksMaterializedViewPreviewInput {
|
||||
name: string;
|
||||
query: string;
|
||||
async?: boolean;
|
||||
comment?: string;
|
||||
distributionColumnNames?: string[];
|
||||
bucketCount?: number;
|
||||
refreshClause?: string;
|
||||
partitionClause?: string;
|
||||
orderByColumnNames?: string[];
|
||||
properties?: string;
|
||||
}
|
||||
|
||||
const escapeSqlString = (value: string) => String(value || '').replace(/'/g, "''");
|
||||
@@ -156,6 +196,20 @@ const buildDorisColumnDefinition = (column: EditableColumnSnapshot, dbType: stri
|
||||
].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
|
||||
};
|
||||
|
||||
const buildStarRocksColumnDefinition = (column: EditableColumnSnapshot): string => {
|
||||
const defaultSql = buildDefaultSql(column.default, 'starrocks');
|
||||
const extraText = String(column.extra || '').trim().toUpperCase();
|
||||
const aggregateSql = DORIS_AGG_TYPES.has(extraText) ? extraText : '';
|
||||
return [
|
||||
quoteIdentifierPart(column.name, 'starrocks'),
|
||||
String(column.type || '').trim(),
|
||||
aggregateSql,
|
||||
column.nullable === 'NO' ? 'NOT NULL' : 'NULL',
|
||||
defaultSql,
|
||||
`COMMENT '${escapeSqlString(column.comment || '')}'`,
|
||||
].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
|
||||
};
|
||||
|
||||
const buildStandardColumnDefinition = (
|
||||
column: EditableColumnSnapshot,
|
||||
dbType: string,
|
||||
@@ -607,6 +661,7 @@ export const buildAlterTablePreviewSql = (input: BuildAlterTablePreviewInput): s
|
||||
if (dbType === 'sqlite') return buildSqliteAlterPreviewSql({ ...input, dbType });
|
||||
if (dbType === 'duckdb') return buildDuckDbAlterPreviewSql({ ...input, dbType });
|
||||
if (dbType === 'diros') return buildDorisAlterPreviewSql({ ...input, dbType }, dbType);
|
||||
if (dbType === 'starrocks') return buildLimitedBacktickAlterPreviewSql({ ...input, dbType }, dbType, 'StarRocks');
|
||||
if (dbType === 'clickhouse') return buildLimitedBacktickAlterPreviewSql({ ...input, dbType }, dbType, 'ClickHouse');
|
||||
if (dbType === 'tdengine') return buildLimitedBacktickAlterPreviewSql({ ...input, dbType }, dbType, 'TDengine');
|
||||
if (isMysqlFamilyDialect(dbType)) return buildMySqlAlterPreviewSql({ ...input, dbType }, dbType);
|
||||
@@ -647,8 +702,150 @@ const buildCreateColumnComments = (tableRef: string, input: BuildCreateTablePrev
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
const normalizeStarRocksKeyModel = (value: unknown): StarRocksKeyModel => {
|
||||
const normalized = String(value || '').trim().toUpperCase();
|
||||
if (normalized === 'PRIMARY' || normalized === 'UNIQUE' || normalized === 'AGGREGATE') return normalized;
|
||||
return 'DUPLICATE';
|
||||
};
|
||||
|
||||
const normalizeStarRocksDistributionType = (value: unknown): StarRocksDistributionType => {
|
||||
const normalized = String(value || '').trim().toUpperCase();
|
||||
if (normalized === 'RANDOM' || normalized === 'NONE') return normalized;
|
||||
return 'HASH';
|
||||
};
|
||||
|
||||
const pickStarRocksKeyColumns = (
|
||||
input: BuildCreateTablePreviewInput,
|
||||
options: StarRocksCreateTableOptions,
|
||||
): string[] => {
|
||||
const requested = Array.isArray(options.keyColumnNames) ? options.keyColumnNames : [];
|
||||
const fallback = input.columns.filter((column) => column.key === 'PRI').map((column) => column.name);
|
||||
const source = requested.length > 0 ? requested : (fallback.length > 0 ? fallback : input.columns.slice(0, 1).map((column) => column.name));
|
||||
return source.map((columnName) => String(columnName || '').trim()).filter(Boolean);
|
||||
};
|
||||
|
||||
const quoteStarRocksColumnList = (columnNames: string[]): string => (
|
||||
columnNames.map((columnName) => quoteIdentifierPart(columnName, 'starrocks')).filter(Boolean).join(', ')
|
||||
);
|
||||
|
||||
const normalizeStarRocksPropertiesBlock = (raw: unknown): string => {
|
||||
const lines = String(raw || '')
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim().replace(/,+$/, ''))
|
||||
.filter(Boolean);
|
||||
if (lines.length === 0) return '';
|
||||
return `PROPERTIES (\n ${lines.join(',\n ')}\n)`;
|
||||
};
|
||||
|
||||
const buildStarRocksDistributionSql = (
|
||||
input: BuildCreateTablePreviewInput,
|
||||
options: StarRocksCreateTableOptions,
|
||||
keyColumns: string[],
|
||||
): string => {
|
||||
const distributionType = normalizeStarRocksDistributionType(options.distributionType);
|
||||
if (distributionType === 'NONE') return '';
|
||||
if (distributionType === 'RANDOM') {
|
||||
return options.bucketMode === 'NUMBER' && Number(options.bucketCount) > 0
|
||||
? `DISTRIBUTED BY RANDOM BUCKETS ${Number(options.bucketCount)}`
|
||||
: 'DISTRIBUTED BY RANDOM BUCKETS AUTO';
|
||||
}
|
||||
|
||||
const requested = Array.isArray(options.distributionColumnNames) ? options.distributionColumnNames : [];
|
||||
const distributionColumns = requested.length > 0 ? requested : keyColumns;
|
||||
const columnList = quoteStarRocksColumnList(
|
||||
distributionColumns.length > 0 ? distributionColumns : input.columns.slice(0, 1).map((column) => column.name)
|
||||
);
|
||||
if (!columnList) return '';
|
||||
|
||||
const bucketSql = options.bucketMode === 'NUMBER' && Number(options.bucketCount) > 0
|
||||
? `BUCKETS ${Number(options.bucketCount)}`
|
||||
: 'BUCKETS AUTO';
|
||||
return `DISTRIBUTED BY HASH(${columnList}) ${bucketSql}`;
|
||||
};
|
||||
|
||||
const buildStarRocksRollupSql = (tableRef: string, rollups: StarRocksRollupOption[] | undefined): string[] => (
|
||||
(Array.isArray(rollups) ? rollups : [])
|
||||
.map((rollup) => {
|
||||
const rollupName = String(rollup?.name || '').trim();
|
||||
const columnList = quoteStarRocksColumnList(Array.isArray(rollup?.columnNames) ? rollup.columnNames : []);
|
||||
if (!rollupName || !columnList) return '';
|
||||
const fromSql = String(rollup.fromIndexName || '').trim()
|
||||
? ` FROM ${quoteIdentifierPart(String(rollup.fromIndexName || '').trim(), 'starrocks')}`
|
||||
: '';
|
||||
const propertiesSql = normalizeStarRocksPropertiesBlock(rollup.properties);
|
||||
const suffix = propertiesSql ? `\n${propertiesSql}` : '';
|
||||
return `ALTER TABLE ${tableRef}\nADD ROLLUP ${quoteIdentifierPart(rollupName, 'starrocks')} (${columnList})${fromSql}${suffix};`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
const buildStarRocksCreateTablePreviewSql = (input: BuildCreateTablePreviewInput): string => {
|
||||
const options = input.starRocksOptions || {};
|
||||
const tableRef = quoteIdentifierPath(input.tableName, 'starrocks');
|
||||
const colDefs = input.columns.map((column) => buildStarRocksColumnDefinition(column));
|
||||
const createPrefix = options.tableKind === 'external' ? 'CREATE EXTERNAL TABLE' : 'CREATE TABLE';
|
||||
const createSql = `${createPrefix} ${tableRef} (\n ${colDefs.join(',\n ')}\n)`;
|
||||
|
||||
if (options.tableKind === 'external') {
|
||||
const engine = String(options.externalEngine || 'hive').trim().toUpperCase();
|
||||
const propertiesSql = normalizeStarRocksPropertiesBlock(options.externalProperties || options.properties);
|
||||
return `${createSql}\nENGINE=${engine}${propertiesSql ? `\n${propertiesSql}` : ''};`;
|
||||
}
|
||||
|
||||
const keyModel = normalizeStarRocksKeyModel(options.keyModel);
|
||||
const keyColumns = pickStarRocksKeyColumns(input, options);
|
||||
const keyColumnSql = quoteStarRocksColumnList(keyColumns);
|
||||
const keySql = keyColumnSql ? `${keyModel} KEY (${keyColumnSql})` : '';
|
||||
const partitionSql = String(options.partitionClause || '').trim().replace(/;+\s*$/, '');
|
||||
const distributionSql = buildStarRocksDistributionSql(input, options, keyColumns);
|
||||
const propertiesSql = normalizeStarRocksPropertiesBlock(options.properties);
|
||||
|
||||
const clauses = [
|
||||
'ENGINE=OLAP',
|
||||
keySql,
|
||||
partitionSql,
|
||||
distributionSql,
|
||||
propertiesSql,
|
||||
].filter(Boolean);
|
||||
const createStatement = `${createSql}\n${clauses.join('\n')};`;
|
||||
const rollupStatements = buildStarRocksRollupSql(tableRef, options.rollups);
|
||||
return [createStatement, ...rollupStatements].join('\n');
|
||||
};
|
||||
|
||||
export const buildStarRocksMaterializedViewPreviewSql = (
|
||||
input: BuildStarRocksMaterializedViewPreviewInput,
|
||||
): string => {
|
||||
const name = quoteIdentifierPath(input.name || 'mv_name', 'starrocks');
|
||||
const query = String(input.query || '').trim().replace(/;+\s*$/, '') || 'SELECT column1, COUNT(*) AS cnt\nFROM table_name\nGROUP BY column1';
|
||||
const commentSql = String(input.comment || '').trim() ? `\nCOMMENT '${escapeSqlString(String(input.comment || '').trim())}'` : '';
|
||||
const refreshSql = String(input.refreshClause || '').trim()
|
||||
|| (input.async === false ? 'REFRESH MANUAL' : 'REFRESH ASYNC');
|
||||
const partitionSql = String(input.partitionClause || '').trim().replace(/;+\s*$/, '');
|
||||
const distributionColumns = quoteStarRocksColumnList(Array.isArray(input.distributionColumnNames) ? input.distributionColumnNames : []);
|
||||
const distributionSql = distributionColumns
|
||||
? `DISTRIBUTED BY HASH(${distributionColumns}) BUCKETS ${Number(input.bucketCount) > 0 ? Number(input.bucketCount) : 'AUTO'}`
|
||||
: '';
|
||||
const orderByColumns = quoteStarRocksColumnList(Array.isArray(input.orderByColumnNames) ? input.orderByColumnNames : []);
|
||||
const orderBySql = orderByColumns ? `ORDER BY (${orderByColumns})` : '';
|
||||
const propertiesSql = normalizeStarRocksPropertiesBlock(input.properties);
|
||||
return [
|
||||
`CREATE MATERIALIZED VIEW ${name}${commentSql}`,
|
||||
refreshSql,
|
||||
partitionSql,
|
||||
distributionSql,
|
||||
orderBySql,
|
||||
propertiesSql,
|
||||
'AS',
|
||||
`${query};`,
|
||||
].filter(Boolean).join('\n');
|
||||
};
|
||||
|
||||
export const buildCreateTablePreviewSql = (input: BuildCreateTablePreviewInput): string => {
|
||||
const dbType = resolveSqlDialect(input.dbType);
|
||||
if (dbType === 'starrocks') {
|
||||
return buildStarRocksCreateTablePreviewSql({ ...input, dbType });
|
||||
}
|
||||
|
||||
const tableRef = quoteIdentifierPath(input.tableName, dbType);
|
||||
const colDefs = input.columns.map((column) => buildCreateTableColumnDefinition(column, dbType));
|
||||
const pkColumns = input.columns.filter((column) => column.key === 'PRI');
|
||||
|
||||
@@ -240,6 +240,28 @@ describe('store appearance persistence', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps StarRocks saved connections as independent datasource type', async () => {
|
||||
const { useStore } = await importStore();
|
||||
|
||||
useStore.getState().replaceConnections([
|
||||
{
|
||||
id: 'starrocks-fe',
|
||||
name: 'StarRocks FE',
|
||||
config: {
|
||||
id: 'starrocks-fe',
|
||||
type: 'starrocks',
|
||||
host: 'starrocks.local',
|
||||
port: 9030,
|
||||
user: 'root',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const config = useStore.getState().connections[0]?.config;
|
||||
expect(config?.type).toBe('starrocks');
|
||||
expect(config?.port).toBe(9030);
|
||||
});
|
||||
|
||||
it('normalizes OceanBase protocol override when replacing saved connections', async () => {
|
||||
const { useStore } = await importStore();
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ const SUPPORTED_CONNECTION_TYPES = new Set([
|
||||
"oceanbase",
|
||||
"doris",
|
||||
"diros",
|
||||
"starrocks",
|
||||
"sphinx",
|
||||
"clickhouse",
|
||||
"postgres",
|
||||
@@ -133,6 +134,7 @@ const SSL_SUPPORTED_CONNECTION_TYPES = new Set([
|
||||
"mariadb",
|
||||
"oceanbase",
|
||||
"diros",
|
||||
"starrocks",
|
||||
"sphinx",
|
||||
"dameng",
|
||||
"clickhouse",
|
||||
@@ -159,6 +161,7 @@ const getDefaultPortByType = (type: string): number => {
|
||||
return 2881;
|
||||
case "doris":
|
||||
case "diros":
|
||||
case "starrocks":
|
||||
return 9030;
|
||||
case "duckdb":
|
||||
return 0;
|
||||
|
||||
@@ -419,6 +419,7 @@ export interface TabData {
|
||||
redisDB?: number; // Redis database index for redis tabs
|
||||
triggerName?: string; // Trigger name for trigger tabs
|
||||
viewName?: string; // View name for view definition tabs
|
||||
viewKind?: "view" | "materialized";
|
||||
routineName?: string; // Routine name for function/procedure definition tabs
|
||||
routineType?: string; // 'FUNCTION' or 'PROCEDURE'
|
||||
savedQueryId?: string; // Saved query identity for quick-save behavior
|
||||
|
||||
@@ -71,6 +71,7 @@ describe('connectionModalPresentation', () => {
|
||||
'oceanbase',
|
||||
'doris',
|
||||
'diros',
|
||||
'starrocks',
|
||||
'sphinx',
|
||||
'clickhouse',
|
||||
'postgres',
|
||||
|
||||
@@ -59,6 +59,7 @@ const mysqlCompatibleTypes = new Set([
|
||||
'oceanbase',
|
||||
'doris',
|
||||
'diros',
|
||||
'starrocks',
|
||||
'sphinx',
|
||||
]);
|
||||
const postgresCompatibleTypes = new Set([
|
||||
|
||||
@@ -30,6 +30,16 @@ describe('dataSourceCapabilities', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps StarRocks as an independent SQL datasource capability', () => {
|
||||
expect(getDataSourceCapabilities({ type: 'starrocks' })).toMatchObject({
|
||||
type: 'starrocks',
|
||||
supportsQueryEditor: true,
|
||||
supportsSqlQueryExport: true,
|
||||
supportsCopyInsert: true,
|
||||
preferManualTotalCount: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats OceanBase Oracle protocol as Oracle capabilities', () => {
|
||||
expect(getDataSourceCapabilities({
|
||||
type: 'oceanbase',
|
||||
|
||||
@@ -8,6 +8,8 @@ const normalizeDataSourceToken = (raw: string): string => {
|
||||
switch (normalized) {
|
||||
case 'doris':
|
||||
return 'diros';
|
||||
case 'starrocks':
|
||||
return 'starrocks';
|
||||
case 'postgresql':
|
||||
return 'postgres';
|
||||
case 'opengauss':
|
||||
@@ -42,6 +44,7 @@ const SQL_QUERY_EXPORT_TYPES = new Set([
|
||||
'mariadb',
|
||||
'oceanbase',
|
||||
'diros',
|
||||
'starrocks',
|
||||
'sphinx',
|
||||
'postgres',
|
||||
'kingbase',
|
||||
@@ -62,6 +65,7 @@ const COPY_INSERT_TYPES = new Set([
|
||||
'mariadb',
|
||||
'oceanbase',
|
||||
'diros',
|
||||
'starrocks',
|
||||
'sphinx',
|
||||
'postgres',
|
||||
'kingbase',
|
||||
|
||||
@@ -7,4 +7,4 @@ export const DRIVER_LOCAL_IMPORT_SINGLE_FILE_HELP =
|
||||
'行内“导入驱动包”仅用于单个驱动文件/总包(如 `mariadb-driver-agent`、`mariadb-driver-agent.exe`、`GoNavi-DriverAgents.zip`),不支持直接导入 JDBC Jar;批量导入请使用上方“导入驱动目录”。';
|
||||
|
||||
export const CUSTOM_CONNECTION_DRIVER_HELP =
|
||||
'已支持: mysql, oceanbase, postgres, opengauss, sqlite, oracle, dm, kingbase;别名支持 postgresql/pgx、open_gauss/open-gauss、dm8、kingbase8/kingbasees/kingbasev8。当前不支持通过 JDBC Jar 扩展驱动。';
|
||||
'已支持: mysql, starrocks, oceanbase, postgres, opengauss, sqlite, oracle, dm, kingbase;别名支持 postgresql/pgx、open_gauss/open-gauss、dm8、kingbase8/kingbasees/kingbasev8。当前不支持通过 JDBC Jar 扩展驱动。';
|
||||
|
||||
@@ -9,6 +9,7 @@ describe('applyQueryAutoLimit', () => {
|
||||
'oceanbase',
|
||||
'diros',
|
||||
'doris',
|
||||
'starrocks',
|
||||
'sphinx',
|
||||
'postgres',
|
||||
'postgresql',
|
||||
@@ -60,6 +61,7 @@ describe('applyQueryAutoLimit', () => {
|
||||
['mssql', 'SELECT TOP 500 * FROM users'],
|
||||
['postgresql', 'SELECT * FROM users LIMIT 500'],
|
||||
['doris', 'SELECT * FROM users LIMIT 500'],
|
||||
['starrocks', 'SELECT * FROM users LIMIT 500'],
|
||||
['sqlite3', 'SELECT * FROM users LIMIT 500'],
|
||||
])('uses custom driver dialect %s', (driver, expected) => {
|
||||
expect(applyQueryAutoLimit('SELECT * FROM users', 'custom', 500, driver).sql)
|
||||
|
||||
@@ -96,6 +96,28 @@ describe('sidebarLocate', () => {
|
||||
})).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps StarRocks materialized view tabs on the materialized views branch', () => {
|
||||
const request = normalizeSidebarLocateObjectRequestFromTab({
|
||||
id: 'view-def-conn-1-main-sales.mv_daily',
|
||||
type: 'view-def',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'main',
|
||||
viewName: 'sales.mv_daily',
|
||||
viewKind: 'materialized',
|
||||
});
|
||||
|
||||
expect(request).toMatchObject({
|
||||
tableName: 'sales.mv_daily',
|
||||
schemaName: 'sales',
|
||||
objectGroup: 'materializedViews',
|
||||
});
|
||||
|
||||
expect(resolveSidebarLocateTarget(request!, { groupBySchema: true })).toMatchObject({
|
||||
targetKey: 'view-def-conn-1-main-sales.mv_daily',
|
||||
objectGroupKey: 'conn-1-main-schema-sales-materializedViews',
|
||||
});
|
||||
});
|
||||
|
||||
it('finds a locate path from loaded tree data even when the target key is absent', () => {
|
||||
const target = resolveSidebarLocateTarget(
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type SidebarLocateObjectGroup = 'tables' | 'views';
|
||||
export type SidebarLocateObjectGroup = 'tables' | 'views' | 'materializedViews';
|
||||
|
||||
export interface SidebarLocateObjectRequest {
|
||||
tabId?: string;
|
||||
@@ -37,6 +37,7 @@ export interface SidebarLocateTabLike {
|
||||
dbName?: string;
|
||||
tableName?: string;
|
||||
viewName?: string;
|
||||
viewKind?: string;
|
||||
}
|
||||
|
||||
const toTrimmedString = (value: unknown): string => String(value ?? '').trim();
|
||||
@@ -55,12 +56,15 @@ export const splitSidebarQualifiedName = (qualifiedName: string): { schemaName:
|
||||
const inferObjectGroup = (detail: Record<string, unknown>, connectionId: string, dbName: string): SidebarLocateObjectGroup => {
|
||||
const explicitGroup = toTrimmedString(detail.objectGroup);
|
||||
if (explicitGroup === 'views' || explicitGroup === 'view') return 'views';
|
||||
if (explicitGroup === 'materializedViews' || explicitGroup === 'materialized-view') return 'materializedViews';
|
||||
|
||||
const explicitType = toTrimmedString(detail.objectType);
|
||||
if (explicitType === 'view' || explicitType === 'views') return 'views';
|
||||
if (explicitType === 'materialized' || explicitType === 'materialized-view') return 'materializedViews';
|
||||
|
||||
const tabId = toTrimmedString(detail.tabId);
|
||||
const dbNodeKey = `${connectionId}-${dbName}`;
|
||||
if (tabId.startsWith(`${dbNodeKey}-materialized-view-`)) return 'materializedViews';
|
||||
if (tabId.startsWith(`${dbNodeKey}-view-`)) return 'views';
|
||||
|
||||
return 'tables';
|
||||
@@ -103,7 +107,9 @@ export const normalizeSidebarLocateObjectRequestFromTab = (tab: SidebarLocateTab
|
||||
connectionId: tab.connectionId,
|
||||
dbName: tab.dbName,
|
||||
tableName: objectName,
|
||||
objectGroup: tab.type === 'view-def' ? 'views' : undefined,
|
||||
objectGroup: tab.type === 'view-def'
|
||||
? (tab.viewKind === 'materialized' ? 'materializedViews' : 'views')
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -113,9 +119,9 @@ export const resolveSidebarLocateTarget = (
|
||||
): SidebarLocateTarget => {
|
||||
const connectionKey = request.connectionId;
|
||||
const databaseKey = `${request.connectionId}-${request.dbName}`;
|
||||
const fallbackTargetKey = request.objectGroup === 'views'
|
||||
? `${databaseKey}-view-${request.tableName}`
|
||||
: `${databaseKey}-${request.tableName}`;
|
||||
const fallbackTargetKey = request.objectGroup === 'materializedViews'
|
||||
? `${databaseKey}-materialized-view-${request.tableName}`
|
||||
: (request.objectGroup === 'views' ? `${databaseKey}-view-${request.tableName}` : `${databaseKey}-${request.tableName}`);
|
||||
const targetKey = request.tabId || fallbackTargetKey;
|
||||
const schemaSegment = request.schemaName || 'default';
|
||||
const schemaKey = options.groupBySchema ? `${databaseKey}-schema-${schemaSegment}` : undefined;
|
||||
@@ -193,6 +199,11 @@ const matchesLocateObjectNode = (node: SidebarLocateTreeNodeLike, target: Sideba
|
||||
return matchesLocateObjectName(target, toTrimmedString(dataRef.viewName || dataRef.tableName), toTrimmedString(dataRef.schemaName));
|
||||
}
|
||||
|
||||
if (target.objectGroup === 'materializedViews') {
|
||||
if (node.type !== 'materialized-view') return false;
|
||||
return matchesLocateObjectName(target, toTrimmedString(dataRef.viewName || dataRef.tableName), toTrimmedString(dataRef.schemaName));
|
||||
}
|
||||
|
||||
if (node.type !== 'table') return false;
|
||||
return matchesLocateObjectName(target, toTrimmedString(dataRef.tableName), toTrimmedString(dataRef.schemaName));
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ export const quoteIdentPart = (dbType: string, ident: string) => {
|
||||
if (!raw) return raw;
|
||||
const dbTypeLower = (dbType || '').toLowerCase();
|
||||
|
||||
if (dbTypeLower === 'mysql' || dbTypeLower === 'mariadb' || dbTypeLower === 'oceanbase' || dbTypeLower === 'diros' || dbTypeLower === 'sphinx' || dbTypeLower === 'tdengine' || dbTypeLower === 'clickhouse') {
|
||||
if (dbTypeLower === 'mysql' || dbTypeLower === 'mariadb' || dbTypeLower === 'oceanbase' || dbTypeLower === 'diros' || dbTypeLower === 'starrocks' || dbTypeLower === 'sphinx' || dbTypeLower === 'tdengine' || dbTypeLower === 'clickhouse') {
|
||||
return `\`${raw.replace(/`/g, '``')}\``;
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ export const buildOrderBySQL = (
|
||||
// 部分数据源在无显式排序需求时强制 ORDER BY(即使按主键)会显著放大大表预览成本:
|
||||
// MySQL/MariaDB 可能触发 filesort 和 sort memory 错误,DuckDB 大文件可能被排序拖到连接超时。
|
||||
// 因此仅在用户主动点击排序时下发 ORDER BY,默认分页查询不加兜底排序。
|
||||
if (dbTypeLower === 'mysql' || dbTypeLower === 'mariadb' || dbTypeLower === 'oceanbase' || dbTypeLower === 'diros' || dbTypeLower === 'duckdb') {
|
||||
if (dbTypeLower === 'mysql' || dbTypeLower === 'mariadb' || dbTypeLower === 'oceanbase' || dbTypeLower === 'diros' || dbTypeLower === 'starrocks' || dbTypeLower === 'duckdb') {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ describe('sqlDialect', () => {
|
||||
expect(resolveSqlDialect('OpenGauss')).toBe('opengauss');
|
||||
expect(resolveSqlDialect('OceanBase')).toBe('oceanbase');
|
||||
expect(resolveSqlDialect('doris')).toBe('diros');
|
||||
expect(resolveSqlDialect('StarRocks')).toBe('starrocks');
|
||||
expect(resolveSqlDialect('dameng')).toBe('dameng');
|
||||
expect(resolveSqlDialect('custom', 'kingbase8')).toBe('kingbase');
|
||||
expect(resolveSqlDialect('custom', 'dm8')).toBe('dameng');
|
||||
@@ -26,6 +27,7 @@ describe('sqlDialect', () => {
|
||||
expect(resolveSqlDialect('custom', 'oceanbase', { oceanBaseProtocol: 'oracle' })).toBe('oracle');
|
||||
expect(isMysqlFamilyDialect('mariadb')).toBe(true);
|
||||
expect(isMysqlFamilyDialect('oceanbase')).toBe(true);
|
||||
expect(isMysqlFamilyDialect('starrocks')).toBe(true);
|
||||
expect(isMysqlFamilyDialect('oracle')).toBe(false);
|
||||
});
|
||||
|
||||
@@ -38,6 +40,7 @@ describe('sqlDialect', () => {
|
||||
expect(values(resolveColumnTypeOptions('oceanbase'))).toContain('varchar(255)');
|
||||
expect(values(resolveColumnTypeOptions('kingbase'))).not.toContain('tinyint(1)');
|
||||
expect(values(resolveColumnTypeOptions('diros'))).toContain('LARGEINT');
|
||||
expect(values(resolveColumnTypeOptions('starrocks'))).toContain('PERCENTILE');
|
||||
expect(values(resolveColumnTypeOptions('sphinx'))).toContain('text');
|
||||
expect(values(resolveColumnTypeOptions('clickhouse'))).toContain('DateTime64(3)');
|
||||
expect(values(resolveColumnTypeOptions('tdengine'))).toContain('TIMESTAMP');
|
||||
@@ -55,6 +58,8 @@ describe('sqlDialect', () => {
|
||||
it('resolves mysql-family completion keywords and functions with mysql syntax', () => {
|
||||
expect(resolveSqlKeywords('mariadb')).toEqual(expect.arrayContaining(['LIMIT', 'CHANGE', 'AUTO_INCREMENT']));
|
||||
expect(names(resolveSqlFunctions('diros'))).toEqual(expect.arrayContaining(['DATE_FORMAT', 'GROUP_CONCAT']));
|
||||
expect(resolveSqlKeywords('starrocks')).toEqual(expect.arrayContaining(['OLAP', 'DISTRIBUTED BY', 'BUCKETS', 'ADD ROLLUP', 'EXTERNAL CATALOG']));
|
||||
expect(names(resolveSqlFunctions('starrocks'))).toEqual(expect.arrayContaining(['TO_BITMAP', 'HLL_UNION_AGG']));
|
||||
});
|
||||
|
||||
it('resolves sqlserver completion without mysql-only ddl tokens', () => {
|
||||
|
||||
@@ -12,6 +12,7 @@ export type SqlDialect =
|
||||
| 'mariadb'
|
||||
| 'oceanbase'
|
||||
| 'diros'
|
||||
| 'starrocks'
|
||||
| 'sphinx'
|
||||
| 'postgres'
|
||||
| 'kingbase'
|
||||
@@ -70,6 +71,8 @@ export const resolveSqlDialect = (
|
||||
case 'doris':
|
||||
case 'diros':
|
||||
return 'diros';
|
||||
case 'starrocks':
|
||||
return 'starrocks';
|
||||
case 'dm':
|
||||
case 'dm8':
|
||||
case 'dameng':
|
||||
@@ -107,6 +110,7 @@ export const resolveSqlDialect = (
|
||||
if (source.includes('mariadb')) return 'mariadb';
|
||||
if (source.includes('mysql')) return 'mysql';
|
||||
if (source.includes('doris') || source.includes('diros')) return 'diros';
|
||||
if (source.includes('starrocks')) return 'starrocks';
|
||||
if (source.includes('sphinx')) return 'sphinx';
|
||||
if (source.includes('kingbase')) return 'kingbase';
|
||||
if (source.includes('highgo')) return 'highgo';
|
||||
@@ -123,7 +127,7 @@ export const resolveSqlDialect = (
|
||||
};
|
||||
|
||||
export const isMysqlFamilyDialect = (dbType: string): boolean => (
|
||||
['mysql', 'mariadb', 'oceanbase', 'diros', 'sphinx', 'tidb', 'starrocks'].includes(resolveSqlDialect(dbType))
|
||||
['mysql', 'mariadb', 'oceanbase', 'diros', 'starrocks', 'sphinx', 'tidb'].includes(resolveSqlDialect(dbType))
|
||||
);
|
||||
|
||||
export const isPgLikeDialect = (dbType: string): boolean => (
|
||||
@@ -358,6 +362,30 @@ const DORIS_TYPES = optionValues([
|
||||
'STRUCT<name:STRING>',
|
||||
]);
|
||||
|
||||
const STARROCKS_TYPES = optionValues([
|
||||
'BOOLEAN',
|
||||
'TINYINT',
|
||||
'SMALLINT',
|
||||
'INT',
|
||||
'BIGINT',
|
||||
'LARGEINT',
|
||||
'FLOAT',
|
||||
'DOUBLE',
|
||||
'DECIMAL(10,2)',
|
||||
'DATE',
|
||||
'DATETIME',
|
||||
'CHAR(50)',
|
||||
'VARCHAR(255)',
|
||||
'STRING',
|
||||
'JSON',
|
||||
'BITMAP',
|
||||
'HLL',
|
||||
'PERCENTILE',
|
||||
'ARRAY<INT>',
|
||||
'MAP<STRING,STRING>',
|
||||
'STRUCT<name STRING>',
|
||||
]);
|
||||
|
||||
const SPHINX_TYPES = optionValues([
|
||||
'text',
|
||||
'string',
|
||||
@@ -444,6 +472,7 @@ const COMMON_TYPES = optionValues(['int', 'varchar(255)', 'text', 'datetime', 'd
|
||||
export const resolveColumnTypeOptions = (dbType: string): ColumnTypeOption[] => {
|
||||
const dialect = resolveSqlDialect(dbType);
|
||||
if (dialect === 'diros') return DORIS_TYPES;
|
||||
if (dialect === 'starrocks') return STARROCKS_TYPES;
|
||||
if (dialect === 'sphinx') return SPHINX_TYPES;
|
||||
if (isMysqlFamilyDialect(dialect)) return MYSQL_TYPES;
|
||||
if (isPgLikeDialect(dialect)) return PG_TYPES;
|
||||
@@ -495,10 +524,19 @@ const CLICKHOUSE_KEYWORDS = [
|
||||
'SAMPLE', 'MATERIALIZED', 'ALIAS', 'SETTINGS', 'TTL', 'CODEC',
|
||||
];
|
||||
|
||||
const STARROCKS_KEYWORDS = [
|
||||
'LIMIT', 'OFFSET', 'ENGINE', 'OLAP', 'DUPLICATE KEY', 'PRIMARY KEY',
|
||||
'AGGREGATE KEY', 'UNIQUE KEY', 'DISTRIBUTED BY', 'HASH', 'BUCKETS',
|
||||
'PARTITION BY', 'PROPERTIES', 'MATERIALIZED VIEW', 'REFRESH ASYNC',
|
||||
'REFRESH MANUAL', 'ROLLUP', 'ADD ROLLUP', 'EXTERNAL CATALOG',
|
||||
'CREATE EXTERNAL TABLE', 'BITMAP', 'HLL',
|
||||
];
|
||||
|
||||
const TDENGINE_KEYWORDS = ['LIMIT', 'SLIMIT', 'SOFFSET', 'TAGS', 'USING', 'INTERVAL', 'FILL', 'PARTITION BY'];
|
||||
|
||||
export const resolveSqlKeywords = (dbType: string): string[] => {
|
||||
const dialect = resolveSqlDialect(dbType);
|
||||
if (dialect === 'starrocks') return unique([...COMMON_KEYWORDS, ...MYSQL_KEYWORDS, ...STARROCKS_KEYWORDS]);
|
||||
if (isMysqlFamilyDialect(dialect)) return unique([...COMMON_KEYWORDS, ...MYSQL_KEYWORDS]);
|
||||
if (isPgLikeDialect(dialect)) return unique([...COMMON_KEYWORDS, ...PG_KEYWORDS]);
|
||||
if (isOracleLikeDialect(dialect)) return unique([...COMMON_KEYWORDS, ...ORACLE_KEYWORDS]);
|
||||
@@ -692,6 +730,21 @@ const CLICKHOUSE_FUNCTIONS = [
|
||||
fn('toInt64', 'ClickHouse - 转 Int64'),
|
||||
];
|
||||
|
||||
const STARROCKS_FUNCTIONS = [
|
||||
fn('DATE_FORMAT', 'StarRocks - 日期格式化'),
|
||||
fn('STR_TO_DATE', 'StarRocks - 字符串转日期'),
|
||||
fn('FROM_UNIXTIME', 'StarRocks - Unix 时间戳转时间'),
|
||||
fn('TO_BITMAP', 'StarRocks - 构造 Bitmap'),
|
||||
fn('BITMAP_UNION', 'StarRocks - Bitmap 聚合'),
|
||||
fn('BITMAP_COUNT', 'StarRocks - Bitmap 计数'),
|
||||
fn('HLL_HASH', 'StarRocks - HLL 哈希'),
|
||||
fn('HLL_UNION_AGG', 'StarRocks - HLL 聚合'),
|
||||
fn('APPROX_COUNT_DISTINCT', 'StarRocks - 近似去重'),
|
||||
fn('PERCENTILE_APPROX', 'StarRocks - 近似分位数'),
|
||||
fn('GET_JSON_STRING', 'StarRocks - JSON 字符串提取'),
|
||||
fn('ARRAY_LENGTH', 'StarRocks - 数组长度'),
|
||||
];
|
||||
|
||||
const TDENGINE_FUNCTIONS = [
|
||||
fn('NOW', 'TDengine - 当前时间'),
|
||||
fn('TODAY', 'TDengine - 当前日期'),
|
||||
@@ -723,6 +776,7 @@ const mergeFunctions = (items: SqlFunctionCompletion[]): SqlFunctionCompletion[]
|
||||
|
||||
export const resolveSqlFunctions = (dbType: string): SqlFunctionCompletion[] => {
|
||||
const dialect = resolveSqlDialect(dbType);
|
||||
if (dialect === 'starrocks') return mergeFunctions([...COMMON_FUNCTIONS, ...MYSQL_FUNCTIONS, ...STARROCKS_FUNCTIONS]);
|
||||
if (isMysqlFamilyDialect(dialect)) return mergeFunctions([...COMMON_FUNCTIONS, ...MYSQL_FUNCTIONS]);
|
||||
if (isPgLikeDialect(dialect)) return mergeFunctions([...COMMON_FUNCTIONS, ...PG_FUNCTIONS]);
|
||||
if (isOracleLikeDialect(dialect)) return mergeFunctions([...COMMON_FUNCTIONS, ...ORACLE_FUNCTIONS]);
|
||||
|
||||
@@ -20,7 +20,7 @@ func normalizeRunConfig(config connection.ConnectionConfig, dbName string) conne
|
||||
if !isOceanBaseOracleProtocol(config) {
|
||||
runConfig.Database = name
|
||||
}
|
||||
case "mysql", "mariadb", "diros", "sphinx", "postgres", "kingbase", "highgo", "vastbase", "opengauss", "sqlserver", "mongodb", "tdengine", "clickhouse":
|
||||
case "mysql", "mariadb", "diros", "starrocks", "sphinx", "postgres", "kingbase", "highgo", "vastbase", "opengauss", "sqlserver", "mongodb", "tdengine", "clickhouse":
|
||||
// 这些类型的 dbName 表示"数据库",需要写入连接配置以选择目标库。
|
||||
runConfig.Database = name
|
||||
case "dameng":
|
||||
|
||||
@@ -77,6 +77,19 @@ func TestNormalizeRunConfig_OceanBaseOracleKeepsServiceName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRunConfig_StarRocksUsesDatabaseFromTree(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
runConfig := normalizeRunConfig(connection.ConnectionConfig{
|
||||
Type: "starrocks",
|
||||
Database: "default_cluster",
|
||||
}, "analytics")
|
||||
|
||||
if runConfig.Database != "analytics" {
|
||||
t.Fatalf("expected StarRocks database from tree, got %q", runConfig.Database)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeSchemaAndTable_OceanBaseOracleUsesSchemaFromDatabaseTree(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -207,6 +207,8 @@ func defaultPortByType(driverType string) int {
|
||||
return 2881
|
||||
case "diros":
|
||||
return 9030
|
||||
case "starrocks":
|
||||
return 9030
|
||||
case "sphinx":
|
||||
return 9306
|
||||
case "postgres", "vastbase", "opengauss":
|
||||
|
||||
@@ -131,6 +131,8 @@ func (a *App) CreateDatabase(config connection.ConnectionConfig, dbName string)
|
||||
query = fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", quoteIdentByType(dbType, dbName))
|
||||
} else if dbType == "clickhouse" {
|
||||
query = fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", quoteIdentByType(dbType, dbName))
|
||||
} else if dbType == "starrocks" {
|
||||
query = fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", quoteIdentByType(dbType, dbName))
|
||||
} else if dbType == "mariadb" || dbType == "diros" || dbType == "oceanbase" {
|
||||
// MariaDB uses same syntax as MySQL
|
||||
} else if dbType == "sphinx" {
|
||||
@@ -184,6 +186,8 @@ func resolveDDLDBType(config connection.ConnectionConfig) string {
|
||||
return "sqlserver"
|
||||
case "diros", "doris":
|
||||
return "diros"
|
||||
case "starrocks":
|
||||
return "starrocks"
|
||||
case "kingbase", "kingbase8", "kingbasees", "kingbasev8":
|
||||
return "kingbase"
|
||||
case "highgo":
|
||||
@@ -213,6 +217,8 @@ func resolveDDLDBType(config connection.ConnectionConfig) string {
|
||||
return "sqlserver"
|
||||
case strings.Contains(driver, "diros"), strings.Contains(driver, "doris"):
|
||||
return "diros"
|
||||
case strings.Contains(driver, "starrocks"):
|
||||
return "starrocks"
|
||||
case strings.Contains(driver, "oceanbase"):
|
||||
return "oceanbase"
|
||||
default:
|
||||
@@ -277,7 +283,7 @@ func buildRunConfigForDDL(config connection.ConnectionConfig, dbType string, dbN
|
||||
if strings.EqualFold(strings.TrimSpace(config.Type), "custom") {
|
||||
// custom 连接的 dbName 语义依赖 driver,尽量在常见驱动上对齐内置类型行为。
|
||||
switch dbType {
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "postgres", "kingbase", "highgo", "vastbase", "opengauss", "dameng", "sqlserver", "clickhouse":
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "starrocks", "sphinx", "postgres", "kingbase", "highgo", "vastbase", "opengauss", "dameng", "sqlserver", "clickhouse":
|
||||
if strings.TrimSpace(dbName) != "" {
|
||||
runConfig.Database = strings.TrimSpace(dbName)
|
||||
}
|
||||
@@ -312,8 +318,8 @@ func (a *App) RenameDatabase(config connection.ConnectionConfig, oldName string,
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
return connection.QueryResult{Success: true, Message: "数据库重命名成功"}
|
||||
case "mysql", "mariadb", "oceanbase", "sphinx":
|
||||
return connection.QueryResult{Success: false, Message: "MySQL/MariaDB/OceanBase/Sphinx 不支持直接重命名数据库,请新建库后迁移数据"}
|
||||
case "mysql", "mariadb", "oceanbase", "starrocks", "sphinx":
|
||||
return connection.QueryResult{Success: false, Message: "MySQL/MariaDB/OceanBase/StarRocks/Sphinx 不支持直接重命名数据库,请新建库后迁移数据"}
|
||||
case "postgres", "kingbase", "highgo", "vastbase", "opengauss":
|
||||
if strings.EqualFold(strings.TrimSpace(config.Database), oldName) {
|
||||
return connection.QueryResult{Success: false, Message: "当前连接正在使用目标数据库,请先连接到其他数据库后再重命名"}
|
||||
@@ -345,7 +351,7 @@ func (a *App) DropDatabase(config connection.ConnectionConfig, dbName string) co
|
||||
sql string
|
||||
)
|
||||
switch dbType {
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "tdengine", "clickhouse":
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "starrocks", "tdengine", "clickhouse":
|
||||
runConfig = config
|
||||
runConfig.Database = ""
|
||||
sql = fmt.Sprintf("DROP DATABASE %s", quoteIdentByType(dbType, dbName))
|
||||
@@ -384,7 +390,7 @@ func (a *App) RenameTable(config connection.ConnectionConfig, dbName string, old
|
||||
|
||||
dbType := resolveDDLDBType(config)
|
||||
switch dbType {
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "postgres", "kingbase", "sqlite", "duckdb", "oracle", "dameng", "highgo", "vastbase", "opengauss", "sqlserver", "clickhouse":
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "starrocks", "sphinx", "postgres", "kingbase", "sqlite", "duckdb", "oracle", "dameng", "highgo", "vastbase", "opengauss", "sqlserver", "clickhouse":
|
||||
default:
|
||||
return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持重命名表", dbType)}
|
||||
}
|
||||
@@ -398,7 +404,7 @@ func (a *App) RenameTable(config connection.ConnectionConfig, dbName string, old
|
||||
|
||||
var sql string
|
||||
switch dbType {
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "clickhouse":
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "starrocks", "sphinx", "clickhouse":
|
||||
newQualifiedTable := quoteTableIdentByType(dbType, schemaName, newTableName)
|
||||
sql = fmt.Sprintf("RENAME TABLE %s TO %s", oldQualifiedTable, newQualifiedTable)
|
||||
case "sqlserver":
|
||||
@@ -430,7 +436,7 @@ func (a *App) DropTable(config connection.ConnectionConfig, dbName string, table
|
||||
|
||||
dbType := resolveDDLDBType(config)
|
||||
switch dbType {
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "postgres", "kingbase", "sqlite", "duckdb", "oracle", "dameng", "highgo", "vastbase", "opengauss", "sqlserver", "tdengine", "clickhouse":
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "starrocks", "sphinx", "postgres", "kingbase", "sqlite", "duckdb", "oracle", "dameng", "highgo", "vastbase", "opengauss", "sqlserver", "tdengine", "clickhouse":
|
||||
default:
|
||||
return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持删除表", dbType)}
|
||||
}
|
||||
@@ -1047,7 +1053,7 @@ func supportsCreateStatementFallback(dbType string) bool {
|
||||
|
||||
func supportsViewCreateStatementLookup(dbType string) bool {
|
||||
switch dbType {
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "postgres", "kingbase", "highgo", "vastbase", "opengauss", "sqlserver", "oracle", "dameng", "sqlite", "duckdb", "clickhouse":
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "starrocks", "sphinx", "postgres", "kingbase", "highgo", "vastbase", "opengauss", "sqlserver", "oracle", "dameng", "sqlite", "duckdb", "clickhouse":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -1246,7 +1252,7 @@ func (a *App) DropView(config connection.ConnectionConfig, dbName string, viewNa
|
||||
|
||||
dbType := resolveDDLDBType(config)
|
||||
switch dbType {
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "postgres", "kingbase", "sqlite", "duckdb", "oracle", "dameng", "highgo", "vastbase", "opengauss", "sqlserver", "clickhouse":
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "starrocks", "sphinx", "postgres", "kingbase", "sqlite", "duckdb", "oracle", "dameng", "highgo", "vastbase", "opengauss", "sqlserver", "clickhouse":
|
||||
default:
|
||||
return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持删除视图", dbType)}
|
||||
}
|
||||
@@ -1281,7 +1287,7 @@ func (a *App) DropFunction(config connection.ConnectionConfig, dbName string, ro
|
||||
|
||||
dbType := resolveDDLDBType(config)
|
||||
switch dbType {
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "postgres", "kingbase", "oracle", "dameng", "highgo", "vastbase", "opengauss", "sqlserver", "duckdb":
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "starrocks", "sphinx", "postgres", "kingbase", "oracle", "dameng", "highgo", "vastbase", "opengauss", "sqlserver", "duckdb":
|
||||
default:
|
||||
return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持删除函数/存储过程", dbType)}
|
||||
}
|
||||
@@ -1335,7 +1341,7 @@ func (a *App) RenameView(config connection.ConnectionConfig, dbName string, oldN
|
||||
|
||||
var sql string
|
||||
switch dbType {
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "clickhouse":
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "starrocks", "sphinx", "clickhouse":
|
||||
newQualified := quoteTableIdentByType(dbType, schemaName, newName)
|
||||
sql = fmt.Sprintf("RENAME TABLE %s TO %s", oldQualified, newQualified)
|
||||
case "postgres", "kingbase", "highgo", "vastbase", "opengauss":
|
||||
|
||||
@@ -336,6 +336,7 @@ const builtinDriverManifestJSON = `{
|
||||
"mariadb": { "engine": "go", "version": "1.9.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/mariadb" },
|
||||
"oceanbase": { "engine": "go", "version": "1.9.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/oceanbase" },
|
||||
"doris": { "engine": "go", "version": "1.9.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/doris" },
|
||||
"starrocks": { "engine": "go", "version": "1.9.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/starrocks" },
|
||||
"sphinx": { "engine": "go", "version": "1.9.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/sphinx" },
|
||||
"sqlserver": { "engine": "go", "version": "1.9.6", "checksumPolicy": "off", "downloadUrl": "builtin://activate/sqlserver" },
|
||||
"sqlite": { "engine": "go", "version": "1.44.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/sqlite" },
|
||||
@@ -391,6 +392,7 @@ var latestDriverVersionMap = map[string]string{
|
||||
"mariadb": "1.9.3",
|
||||
"oceanbase": "1.9.3",
|
||||
"diros": "1.9.3",
|
||||
"starrocks": "1.9.3",
|
||||
"sphinx": "1.9.3",
|
||||
"sqlserver": "1.9.6",
|
||||
"sqlite": "1.46.1",
|
||||
@@ -412,6 +414,7 @@ var driverGoModulePathMap = map[string]string{
|
||||
"mariadb": "github.com/go-sql-driver/mysql",
|
||||
"oceanbase": "github.com/go-sql-driver/mysql",
|
||||
"diros": "github.com/go-sql-driver/mysql",
|
||||
"starrocks": "github.com/go-sql-driver/mysql",
|
||||
"sphinx": "github.com/go-sql-driver/mysql",
|
||||
"sqlserver": "github.com/microsoft/go-mssqldb",
|
||||
"sqlite": "modernc.org/sqlite",
|
||||
@@ -1472,6 +1475,7 @@ func allDriverDefinitionsWithPackages(packages map[string]pinnedDriverPackage) [
|
||||
buildOptionalGoDriverDefinition("mariadb", "MariaDB", packages),
|
||||
buildOptionalGoDriverDefinition("oceanbase", "OceanBase", packages),
|
||||
buildOptionalGoDriverDefinition("diros", "Doris", packages),
|
||||
buildOptionalGoDriverDefinition("starrocks", "StarRocks", packages),
|
||||
buildOptionalGoDriverDefinition("sphinx", "Sphinx", packages),
|
||||
buildOptionalGoDriverDefinition("sqlserver", "SQL Server", packages),
|
||||
buildOptionalGoDriverDefinition("sqlite", "SQLite", packages),
|
||||
@@ -3780,6 +3784,8 @@ func optionalDriverBuildTag(driverType string, selectedVersion string) (string,
|
||||
return "gonavi_oceanbase_driver", nil
|
||||
case "diros":
|
||||
return "gonavi_diros_driver", nil
|
||||
case "starrocks":
|
||||
return "gonavi_starrocks_driver", nil
|
||||
case "sphinx":
|
||||
return "gonavi_sphinx_driver", nil
|
||||
case "sqlserver":
|
||||
|
||||
@@ -128,6 +128,7 @@ func optionalDriverAgentRevisionTestDrivers(t *testing.T) []string {
|
||||
"mariadb",
|
||||
"oceanbase",
|
||||
"diros",
|
||||
"starrocks",
|
||||
"sphinx",
|
||||
"sqlserver",
|
||||
"sqlite",
|
||||
|
||||
@@ -1251,7 +1251,7 @@ const (
|
||||
|
||||
func supportsTruncateTableForDBType(dbType string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(dbType)) {
|
||||
case "mysql", "mariadb", "oceanbase", "postgres", "kingbase", "highgo", "vastbase", "opengauss", "sqlserver", "oracle", "dameng", "clickhouse", "duckdb":
|
||||
case "mysql", "mariadb", "oceanbase", "starrocks", "postgres", "kingbase", "highgo", "vastbase", "opengauss", "sqlserver", "oracle", "dameng", "clickhouse", "duckdb":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -1386,7 +1386,7 @@ func quoteIdentByType(dbType string, ident string) string {
|
||||
|
||||
dbType = resolveDDLDBType(connection.ConnectionConfig{Type: dbType})
|
||||
switch dbType {
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "tdengine", "clickhouse":
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "starrocks", "sphinx", "tdengine", "clickhouse":
|
||||
return "`" + strings.ReplaceAll(ident, "`", "``") + "`"
|
||||
case "kingbase":
|
||||
return db.QuoteKingbaseIdentifier(ident)
|
||||
@@ -1608,7 +1608,7 @@ func buildListViewQueries(config connection.ConnectionConfig, dbName string) []s
|
||||
dbType := resolveDDLDBType(config)
|
||||
escapedDbName := escapeSQLLiteral(dbName)
|
||||
switch dbType {
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "sphinx":
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "starrocks", "sphinx":
|
||||
queries := []string{
|
||||
fmt.Sprintf(`SELECT TABLE_SCHEMA AS schema_name, TABLE_NAME AS object_name, TABLE_TYPE AS table_type FROM information_schema.tables WHERE TABLE_TYPE='VIEW' AND TABLE_SCHEMA='%s' ORDER BY TABLE_NAME`, escapedDbName),
|
||||
}
|
||||
@@ -1711,7 +1711,7 @@ func buildViewCreateQueries(config connection.ConnectionConfig, dbName, schemaNa
|
||||
escapedDB := escapeSQLLiteral(dbName)
|
||||
|
||||
switch dbType {
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "sphinx":
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "starrocks", "sphinx":
|
||||
if safeSchema == "" {
|
||||
safeSchema = strings.TrimSpace(dbName)
|
||||
}
|
||||
@@ -1991,7 +1991,7 @@ func formatSQLValue(dbType string, v interface{}) string {
|
||||
return "'" + val.Format("2006-01-02 15:04:05") + "'"
|
||||
case string:
|
||||
normalizedType := strings.ToLower(strings.TrimSpace(dbType))
|
||||
if (normalizedType == "mysql" || normalizedType == "oceanbase" || normalizedType == "diros") && isMySQLHexLiteral(val) {
|
||||
if (normalizedType == "mysql" || normalizedType == "oceanbase" || normalizedType == "diros" || normalizedType == "starrocks") && isMySQLHexLiteral(val) {
|
||||
return val
|
||||
}
|
||||
escaped := strings.ReplaceAll(val, "'", "''")
|
||||
|
||||
@@ -6,6 +6,7 @@ func registerOptionalDatabaseFactories() {
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("mariadb"), "mariadb")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("oceanbase"), "oceanbase")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("diros"), "diros", "doris")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("starrocks"), "starrocks")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("sphinx"), "sphinx")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("sqlserver"), "sqlserver")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("sqlite"), "sqlite")
|
||||
|
||||
@@ -6,6 +6,7 @@ func registerOptionalDatabaseFactories() {
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("mariadb"), "mariadb")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("oceanbase"), "oceanbase")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("diros"), "diros", "doris")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("starrocks"), "starrocks")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("sphinx"), "sphinx")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("sqlserver"), "sqlserver")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("sqlite"), "sqlite")
|
||||
|
||||
@@ -7,6 +7,7 @@ func init() {
|
||||
"mariadb": "src-4e1ec648c70c87ea",
|
||||
"oceanbase": "src-8e445fc4899d850f",
|
||||
"diros": "src-74927b3809258666",
|
||||
"starrocks": "src-3b5aad8a32f79b61",
|
||||
"sphinx": "src-269bd60a34df47d3",
|
||||
"sqlserver": "src-84553484c72e7253",
|
||||
"sqlite": "src-762863d48f653b89",
|
||||
|
||||
@@ -24,6 +24,7 @@ var optionalGoDrivers = map[string]struct{}{
|
||||
"mariadb": {},
|
||||
"oceanbase": {},
|
||||
"diros": {},
|
||||
"starrocks": {},
|
||||
"sphinx": {},
|
||||
"sqlserver": {},
|
||||
"sqlite": {},
|
||||
@@ -78,6 +79,8 @@ func driverDisplayName(driverType string) string {
|
||||
return "OceanBase"
|
||||
case "diros":
|
||||
return "Doris"
|
||||
case "starrocks":
|
||||
return "StarRocks"
|
||||
case "sphinx":
|
||||
return "Sphinx"
|
||||
case "postgres":
|
||||
|
||||
@@ -113,7 +113,7 @@ func TestNewCompatibleDriversAreOptionalAgentDrivers(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
SetExternalDriverDownloadDirectory(tmpDir)
|
||||
|
||||
for _, driverType := range []string{"oceanbase", "opengauss", "open_gauss"} {
|
||||
for _, driverType := range []string{"oceanbase", "opengauss", "open_gauss", "starrocks"} {
|
||||
if IsBuiltinDriver(driverType) {
|
||||
t.Fatalf("%s 不应是免安装内置驱动", driverType)
|
||||
}
|
||||
|
||||
210
internal/db/starrocks_impl.go
Normal file
210
internal/db/starrocks_impl.go
Normal file
@@ -0,0 +1,210 @@
|
||||
//go:build gonavi_full_drivers || gonavi_starrocks_driver
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
"GoNavi-Wails/internal/ssh"
|
||||
"GoNavi-Wails/internal/utils"
|
||||
|
||||
mysqlDriver "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
const (
|
||||
starRocksDriverName = "starrocks"
|
||||
defaultStarRocksPort = 9030
|
||||
)
|
||||
|
||||
// StarRocksDB 使用独立 driver 名称接入,底层协议兼容 MySQL。
|
||||
type StarRocksDB struct {
|
||||
MySQLDB
|
||||
}
|
||||
|
||||
func init() {
|
||||
for _, name := range sql.Drivers() {
|
||||
if name == starRocksDriverName {
|
||||
return
|
||||
}
|
||||
}
|
||||
sql.Register(starRocksDriverName, &mysqlDriver.MySQLDriver{})
|
||||
}
|
||||
|
||||
func applyStarRocksURI(config connection.ConnectionConfig) connection.ConnectionConfig {
|
||||
uriText := strings.TrimSpace(config.URI)
|
||||
if uriText == "" {
|
||||
return config
|
||||
}
|
||||
|
||||
parsed, ok := parseMySQLCompatibleURI(uriText, "starrocks", "mysql")
|
||||
if !ok {
|
||||
return config
|
||||
}
|
||||
|
||||
if parsed.User != nil {
|
||||
if config.User == "" {
|
||||
config.User = parsed.User.Username()
|
||||
}
|
||||
if pass, ok := parsed.User.Password(); ok && config.Password == "" {
|
||||
config.Password = pass
|
||||
}
|
||||
}
|
||||
|
||||
if dbName := strings.TrimPrefix(parsed.Path, "/"); dbName != "" && config.Database == "" {
|
||||
config.Database = dbName
|
||||
}
|
||||
|
||||
defaultPort := config.Port
|
||||
if defaultPort <= 0 {
|
||||
defaultPort = defaultStarRocksPort
|
||||
}
|
||||
|
||||
hostsFromURI := make([]string, 0, 4)
|
||||
hostText := strings.TrimSpace(parsed.Host)
|
||||
if hostText != "" {
|
||||
for _, entry := range strings.Split(hostText, ",") {
|
||||
host, port, ok := parseHostPortWithDefault(entry, defaultPort)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
hostsFromURI = append(hostsFromURI, normalizeMySQLAddress(host, port))
|
||||
}
|
||||
}
|
||||
|
||||
if len(config.Hosts) == 0 && len(hostsFromURI) > 0 {
|
||||
config.Hosts = hostsFromURI
|
||||
}
|
||||
if strings.TrimSpace(config.Host) == "" && len(hostsFromURI) > 0 {
|
||||
host, port, ok := parseHostPortWithDefault(hostsFromURI[0], defaultPort)
|
||||
if ok {
|
||||
config.Host = host
|
||||
config.Port = port
|
||||
}
|
||||
}
|
||||
|
||||
if config.Topology == "" {
|
||||
topology := strings.TrimSpace(parsed.Query().Get("topology"))
|
||||
if topology != "" {
|
||||
config.Topology = strings.ToLower(topology)
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func collectStarRocksAddresses(config connection.ConnectionConfig) []string {
|
||||
defaultPort := config.Port
|
||||
if defaultPort <= 0 {
|
||||
defaultPort = defaultStarRocksPort
|
||||
}
|
||||
|
||||
candidates := make([]string, 0, len(config.Hosts)+1)
|
||||
if len(config.Hosts) > 0 {
|
||||
candidates = append(candidates, config.Hosts...)
|
||||
} else {
|
||||
candidates = append(candidates, normalizeMySQLAddress(config.Host, defaultPort))
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(candidates))
|
||||
seen := make(map[string]struct{}, len(candidates))
|
||||
for _, entry := range candidates {
|
||||
host, port, ok := parseHostPortWithDefault(entry, defaultPort)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
normalized := normalizeMySQLAddress(host, port)
|
||||
if _, exists := seen[normalized]; exists {
|
||||
continue
|
||||
}
|
||||
seen[normalized] = struct{}{}
|
||||
result = append(result, normalized)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *StarRocksDB) getDSN(config connection.ConnectionConfig) (string, error) {
|
||||
database := config.Database
|
||||
protocol := "tcp"
|
||||
address := normalizeMySQLAddress(config.Host, config.Port)
|
||||
|
||||
if config.UseSSH {
|
||||
netName, err := ssh.RegisterSSHNetwork(config.SSH)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建 SSH 隧道失败:%w", err)
|
||||
}
|
||||
protocol = netName
|
||||
}
|
||||
|
||||
return buildMySQLCompatibleDSN(config, protocol, address, database), nil
|
||||
}
|
||||
|
||||
func resolveStarRocksCredential(config connection.ConnectionConfig, addressIndex int) (string, string) {
|
||||
primaryUser := strings.TrimSpace(config.User)
|
||||
primaryPassword := config.Password
|
||||
replicaUser := strings.TrimSpace(config.MySQLReplicaUser)
|
||||
replicaPassword := config.MySQLReplicaPassword
|
||||
|
||||
if addressIndex > 0 && replicaUser != "" {
|
||||
return replicaUser, replicaPassword
|
||||
}
|
||||
|
||||
if primaryUser == "" && replicaUser != "" {
|
||||
return replicaUser, replicaPassword
|
||||
}
|
||||
|
||||
return config.User, primaryPassword
|
||||
}
|
||||
|
||||
func (s *StarRocksDB) Connect(config connection.ConnectionConfig) error {
|
||||
runConfig := applyStarRocksURI(config)
|
||||
addresses := collectStarRocksAddresses(runConfig)
|
||||
if len(addresses) == 0 {
|
||||
return fmt.Errorf("连接建立后验证失败:未找到可用的 StarRocks 地址")
|
||||
}
|
||||
|
||||
var errorDetails []string
|
||||
for index, address := range addresses {
|
||||
candidateConfig := runConfig
|
||||
host, port, ok := parseHostPortWithDefault(address, defaultStarRocksPort)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
candidateConfig.Host = host
|
||||
candidateConfig.Port = port
|
||||
candidateConfig.User, candidateConfig.Password = resolveStarRocksCredential(runConfig, index)
|
||||
|
||||
dsn, err := s.getDSN(candidateConfig)
|
||||
if err != nil {
|
||||
errorDetails = append(errorDetails, fmt.Sprintf("%s 生成连接串失败: %v", address, err))
|
||||
continue
|
||||
}
|
||||
db, err := sql.Open(starRocksDriverName, dsn)
|
||||
if err != nil {
|
||||
errorDetails = append(errorDetails, fmt.Sprintf("%s 打开失败: %v", address, err))
|
||||
continue
|
||||
}
|
||||
|
||||
timeout := getConnectTimeout(candidateConfig)
|
||||
ctx, cancel := utils.ContextWithTimeout(timeout)
|
||||
pingErr := db.PingContext(ctx)
|
||||
cancel()
|
||||
if pingErr != nil {
|
||||
_ = db.Close()
|
||||
errorDetails = append(errorDetails, fmt.Sprintf("%s 验证失败: %v", address, pingErr))
|
||||
continue
|
||||
}
|
||||
|
||||
s.conn = db
|
||||
s.pingTimeout = timeout
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(errorDetails) == 0 {
|
||||
return fmt.Errorf("连接建立后验证失败:未找到可用的 StarRocks 地址")
|
||||
}
|
||||
return fmt.Errorf("连接建立后验证失败:%s", strings.Join(errorDetails, ";"))
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func classifyMigrationDataModel(dbType string) MigrationDataModel {
|
||||
return MigrationDataModelRelational
|
||||
case "mongodb":
|
||||
return MigrationDataModelDocument
|
||||
case "clickhouse", "diros", "sphinx":
|
||||
case "clickhouse", "diros", "starrocks", "sphinx":
|
||||
return MigrationDataModelColumnar
|
||||
case "tdengine":
|
||||
return MigrationDataModelTimeSeries
|
||||
|
||||
@@ -15,6 +15,7 @@ func TestClassifyMigrationDataModel(t *testing.T) {
|
||||
"kingbase": MigrationDataModelRelational,
|
||||
"mongodb": MigrationDataModelDocument,
|
||||
"clickhouse": MigrationDataModelColumnar,
|
||||
"starrocks": MigrationDataModelColumnar,
|
||||
"tdengine": MigrationDataModelTimeSeries,
|
||||
"redis": MigrationDataModelKeyValue,
|
||||
"custom": MigrationDataModelCustom,
|
||||
|
||||
@@ -45,6 +45,8 @@ func resolveMigrationDBType(config connection.ConnectionConfig) string {
|
||||
return "sphinx"
|
||||
case "diros", "doris":
|
||||
return "diros"
|
||||
case "starrocks":
|
||||
return "starrocks"
|
||||
case "kingbase", "kingbase8", "kingbasees", "kingbasev8":
|
||||
return "kingbase"
|
||||
case "highgo":
|
||||
@@ -76,6 +78,8 @@ func resolveMigrationDBType(config connection.ConnectionConfig) string {
|
||||
return "sphinx"
|
||||
case strings.Contains(driver, "diros"), strings.Contains(driver, "doris"):
|
||||
return "diros"
|
||||
case strings.Contains(driver, "starrocks"):
|
||||
return "starrocks"
|
||||
case strings.Contains(driver, "maria"):
|
||||
return "mariadb"
|
||||
case strings.Contains(driver, "oceanbase"):
|
||||
@@ -91,7 +95,7 @@ func resolveMigrationDBType(config connection.ConnectionConfig) string {
|
||||
|
||||
func isMySQLCoreType(dbType string) bool {
|
||||
switch normalizeMigrationDBType(dbType) {
|
||||
case "mysql", "mariadb", "oceanbase", "diros":
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "starrocks":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -26,7 +26,7 @@ func quoteIdentByType(dbType string, ident string) string {
|
||||
}
|
||||
|
||||
switch normalizeMigrationDBType(dbType) {
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "clickhouse", "tdengine":
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "starrocks", "sphinx", "clickhouse", "tdengine":
|
||||
return "`" + strings.ReplaceAll(ident, "`", "``") + "`"
|
||||
case "kingbase":
|
||||
return db.QuoteKingbaseIdentifier(ident)
|
||||
@@ -140,7 +140,7 @@ func qualifiedNameForQuery(dbType string, schema string, table string, original
|
||||
return raw
|
||||
}
|
||||
return s + "." + table
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "clickhouse", "tdengine":
|
||||
case "mysql", "mariadb", "oceanbase", "diros", "starrocks", "sphinx", "clickhouse", "tdengine":
|
||||
s := strings.TrimSpace(schema)
|
||||
if s == "" || table == "" {
|
||||
return table
|
||||
|
||||
@@ -5,7 +5,7 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
DEFAULT_DRIVERS=(mariadb oceanbase diros sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss mongodb tdengine clickhouse)
|
||||
DEFAULT_DRIVERS=(mariadb oceanbase diros starrocks sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss mongodb tdengine clickhouse)
|
||||
OUTPUT_FILE="internal/db/driver_agent_revisions_gen.go"
|
||||
|
||||
usage() {
|
||||
@@ -27,7 +27,7 @@ normalize_driver() {
|
||||
doris|diros) echo "diros" ;;
|
||||
oceanbase) echo "oceanbase" ;;
|
||||
opengauss|open_gauss|open-gauss) echo "opengauss" ;;
|
||||
mariadb|diros|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|mongodb|tdengine|clickhouse)
|
||||
mariadb|diros|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|mongodb|tdengine|clickhouse)
|
||||
echo "$value"
|
||||
;;
|
||||
*)
|
||||
@@ -97,6 +97,8 @@ oceanbase:internal/db/oracle_impl.go|\
|
||||
oceanbase:internal/db/mysql_impl.go|\
|
||||
diros:internal/db/diros_impl.go|\
|
||||
diros:internal/db/mysql_impl.go|\
|
||||
starrocks:internal/db/starrocks_impl.go|\
|
||||
starrocks:internal/db/mysql_impl.go|\
|
||||
sphinx:internal/db/sphinx_impl.go|\
|
||||
sphinx:internal/db/mysql_impl.go|\
|
||||
sqlserver:internal/db/sqlserver_impl.go|\
|
||||
|
||||
Reference in New Issue
Block a user