mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-04 21:07:45 +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:
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user