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:
Syngnat
2026-05-15 17:30:08 +08:00
parent 2580e4d6f3
commit 569edbb11a
49 changed files with 1164 additions and 62 deletions

View File

@@ -71,6 +71,7 @@ describe('connectionModalPresentation', () => {
'oceanbase',
'doris',
'diros',
'starrocks',
'sphinx',
'clickhouse',
'postgres',

View File

@@ -59,6 +59,7 @@ const mysqlCompatibleTypes = new Set([
'oceanbase',
'doris',
'diros',
'starrocks',
'sphinx',
]);
const postgresCompatibleTypes = new Set([

View File

@@ -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',

View File

@@ -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',

View File

@@ -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 扩展驱动。';

View File

@@ -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)

View File

@@ -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(
{

View File

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

View File

@@ -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 '';
}

View File

@@ -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', () => {

View File

@@ -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]);