mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-15 10:59:41 +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:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user