feat(iotdb): 新增 Apache IoTDB 时序库连接支持

Refs #546
This commit is contained in:
Syngnat
2026-06-13 18:23:56 +08:00
parent c805b16fcd
commit f3dfffb8d1
45 changed files with 1292 additions and 45 deletions

View File

@@ -64,6 +64,18 @@ describe('ConnectionModal data source registry', () => {
expect(source).toContain('return "http://127.0.0.1:6333";');
expect(source).toContain('return "apiKey=...";');
});
it('exposes Apache IoTDB in the create-connection picker with timeseries defaults', () => {
expect(source).toContain("case 'iotdb':");
expect(source).toContain('return 6667;');
expect(source).toContain('iotdb: ["iotdb"]');
expect(source).toContain("key: 'iotdb'");
expect(source).toContain("name: 'Apache IoTDB'");
expect(source).toContain('dbType === "iotdb"');
expect(source).toContain("return 'Storage Group / Device / Timeseries';");
expect(source).toContain('return "iotdb://root:root@127.0.0.1:6667/root.sg";');
expect(source).toContain('return "fetchSize=1024&timeZone=Asia%2FShanghai";');
});
});
describe('ConnectionModal Redis Sentinel configuration', () => {

View File

@@ -1873,6 +1873,9 @@ const ConnectionModal: React.FC<{
if (dbType === "qdrant") {
return "http://127.0.0.1:6333";
}
if (dbType === "iotdb") {
return "iotdb://root:root@127.0.0.1:6667/root.sg";
}
if (dbType === "redis") {
return "redis://:pass@127.0.0.1:6379,127.0.0.2:6379/0?topology=cluster 或 redis://:pass@10.0.0.1:26379,10.0.0.2:26379/0?topology=sentinel&master=mymaster";
}
@@ -1922,6 +1925,8 @@ const ConnectionModal: React.FC<{
return "schema=SYSDBA";
case "tdengine":
return "timezone=Asia%2FShanghai";
case "iotdb":
return "fetchSize=1024&timeZone=Asia%2FShanghai";
default:
return "key=value&another=value";
}

View File

@@ -7,6 +7,7 @@ import { SavedConnection } from '../types';
import { EventsOn } from '../../wailsjs/runtime/runtime';
import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues, resolveTextInputSafeBackdropFilter } from '../utils/appearance';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { quoteIdentPart, quoteQualifiedIdent } from '../utils/sql';
import { formatLocalDateTimeLiteral, normalizeTemporalLiteralText } from './dataGridCopyInsert';
import { buildDataSyncRequest, type SourceDatasetMode, validateDataSyncSelection } from './dataSyncRequest';
const { Title, Text } = Typography;
@@ -46,26 +47,11 @@ type TableOps = {
type WorkflowType = 'sync' | 'migration';
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 === 'starrocks' || t === 'sphinx' || t === 'clickhouse' || t === 'tdengine') {
return `\`${raw.replace(/`/g, '``')}\``;
}
if (t === 'sqlserver') {
return `[${raw.replace(/]/g, ']]')}]`;
}
return `"${raw.replace(/"/g, '""')}"`;
return quoteIdentPart(dbType, String(ident || '').trim());
};
const quoteSqlTable = (dbType: string, tableName: string): string => {
const raw = String(tableName || '').trim();
if (!raw) return raw;
if (!raw.includes('.')) return quoteSqlIdent(dbType, raw);
return raw
.split('.')
.map((part) => quoteSqlIdent(dbType, part))
.join('.');
return quoteQualifiedIdent(dbType, String(tableName || '').trim());
};
const toSqlLiteral = (value: any, dbType: string): string => {

View File

@@ -32,6 +32,13 @@ describe('DatabaseIcons', () => {
expect(markup).toContain('>Qd</text>');
});
it('includes Apache IoTDB in the selectable database icons', () => {
expect(DB_ICON_TYPES).toContain('iotdb');
expect(getDbIconLabel('iotdb')).toBe('Apache IoTDB');
const markup = renderToStaticMarkup(<>{getDbIcon('iotdb', undefined, 22)}</>);
expect(markup).toContain('>Io</text>');
});
it('wraps database icons in a consistent frame for sidebar sizing', () => {
const mysqlMarkup = renderToStaticMarkup(<>{getDbIcon('mysql', undefined, 22)}</>);
const jvmMarkup = renderToStaticMarkup(<>{getDbIcon('jvm', undefined, 22)}</>);

View File

@@ -49,6 +49,7 @@ const DB_DEFAULT_COLORS: Record<string, string> = {
highgo: '#00A86B',
iris: '#1F6FEB',
tdengine: '#2962FF',
iotdb: '#0F766E',
chroma: '#7C3AED',
qdrant: '#DC244C',
diros: '#0050B3',
@@ -180,6 +181,9 @@ const IrisIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
const TDengineIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
<ColorBadge size={size} color={color || DB_DEFAULT_COLORS.tdengine} label="TD" />
);
const IoTDBIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
<ColorBadge size={size} color={color || DB_DEFAULT_COLORS.iotdb} label="Io" />
);
const ChromaIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
<ColorBadge size={size} color={color || DB_DEFAULT_COLORS.chroma} label="Ch" />
);
@@ -239,6 +243,7 @@ const DB_ICON_MAP: Record<string, React.FC<DbIconProps>> = {
highgo: HighGoIcon,
iris: IrisIcon,
tdengine: TDengineIcon,
iotdb: IoTDBIcon,
chroma: ChromaIcon,
qdrant: QdrantIcon,
elasticsearch: ElasticsearchIcon,
@@ -249,7 +254,7 @@ const DB_ICON_MAP: Record<string, React.FC<DbIconProps>> = {
export const DB_ICON_TYPES: string[] = [
'mysql', 'mariadb', 'oceanbase', 'postgres', 'redis', 'mongodb', 'jvm',
'oracle', 'sqlserver', 'sqlite', 'duckdb', 'clickhouse', 'starrocks',
'kingbase', 'dameng', 'vastbase', 'opengauss', 'highgo', 'iris', 'tdengine', 'chroma', 'qdrant', 'elasticsearch', 'custom',
'kingbase', 'dameng', 'vastbase', 'opengauss', 'highgo', 'iris', 'tdengine', 'iotdb', 'chroma', 'qdrant', 'elasticsearch', 'custom',
];
/** 该类型是否有品牌 SVG 文件 */
@@ -271,7 +276,7 @@ export const getDbIconLabel = (type: string): string => {
sqlserver: 'SQL Server', clickhouse: 'ClickHouse', sqlite: 'SQLite',
starrocks: 'StarRocks',
duckdb: 'DuckDB', kingbase: '金仓', dameng: '达梦',
vastbase: 'VastBase', opengauss: 'OpenGauss', highgo: '瀚高', iris: 'InterSystems IRIS', tdengine: 'TDengine',
vastbase: 'VastBase', opengauss: 'OpenGauss', highgo: '瀚高', iris: 'InterSystems IRIS', tdengine: 'TDengine', iotdb: 'Apache IoTDB',
chroma: 'Chroma',
qdrant: 'Qdrant',
elasticsearch: 'Elasticsearch',

View File

@@ -2540,16 +2540,16 @@ const Sidebar: React.FC<{
}
};
const isNonRelationalDbType = (connectionId: string): boolean => {
const isStructureOnlyDbType = (connectionId: string): boolean => {
const conn = connections.find(c => c.id === connectionId);
if (!conn) return false;
const dbType = resolveDataSourceType(conn.config);
return dbType === 'elasticsearch' || dbType === 'mongodb' || dbType === 'redis';
return dbType === 'elasticsearch' || dbType === 'mongodb' || dbType === 'redis' || dbType === 'iotdb';
};
const openDesign = (node: any, initialTab: string, readOnly: boolean = false) => {
const { tableName, dbName, id } = node.dataRef;
const forceReadOnly = readOnly || isNonRelationalDbType(id);
const forceReadOnly = readOnly || isStructureOnlyDbType(id);
addTab({
id: `design-${id}-${dbName}-${tableName}`,
title: `${forceReadOnly ? '表结构' : '设计表'} (${tableName})`,
@@ -2564,6 +2564,10 @@ const Sidebar: React.FC<{
const openNewTableDesign = (node: any) => {
const { dbName, id } = node.dataRef;
if (isStructureOnlyDbType(id)) {
message.warning('当前数据源暂不支持可视化新建表');
return;
}
addTab({
id: `new-table-${id}-${dbName}-${Date.now()}`,
title: `新建表 - ${dbName}`,
@@ -6405,14 +6409,15 @@ const Sidebar: React.FC<{
const groupData = node.dataRef; // { ...conn, dbName, groupKey }
const sortPreferenceKey = `${groupData.id}-${groupData.dbName}`;
const currentSort = tableSortPreference[sortPreferenceKey] || 'name';
const canCreateTable = !isStructureOnlyDbType(String(groupData.id || ''));
return [
{
...(canCreateTable ? [{
key: 'new-table',
label: '新建表',
icon: <TableOutlined />,
onClick: () => openNewTableDesign(node)
},
}] : []),
{ type: 'divider' },
{
key: 'sort-by-name',
@@ -6845,13 +6850,14 @@ const Sidebar: React.FC<{
const capabilities = getDataSourceCapabilities(databaseConn?.config);
const isStarRocks = dialect === 'starrocks';
const supportsSchemaActions = isPostgresSchemaDialect(dialect);
const canCreateTable = !isStructureOnlyDbType(String(databaseConn?.id || ''));
return [
{
...(canCreateTable ? [{
key: 'new-table',
label: '新建表',
icon: <TableOutlined />,
onClick: () => openNewTableDesign(node)
},
}] : []),
...(supportsSchemaActions ? [
{
key: 'new-schema',
@@ -7116,7 +7122,7 @@ const Sidebar: React.FC<{
{ type: 'divider' },
{
key: 'design-table',
label: '设计表',
label: isStructureOnlyDbType(String(node.dataRef?.id || '')) ? '表结构' : '设计表',
icon: <EditOutlined />,
onClick: () => openDesign(node, 'columns', false)
},

View File

@@ -132,6 +132,11 @@ const getMetadataDialect = (connType: string, driver?: string, oceanBaseProtocol
const buildTableStatusSQL = (dialect: string, dbName: string, schemaName?: string): string => {
const escapeLiteral = (s: string) => s.replace(/'/g, "''");
const iotdbDevicePattern = (name: string) => {
const normalized = String(name || '').trim().replace(/[`"]/g, '');
if (!normalized) return '';
return normalized.endsWith('.**') ? normalized : `${normalized}.**`;
};
switch (dialect) {
case 'mysql':
case 'starrocks':
@@ -190,6 +195,10 @@ ORDER BY s.name, t.name`;
return `SELECT name AS table_name, comment AS table_comment, total_rows AS table_rows, total_bytes AS data_length, 0 AS index_length FROM system.tables WHERE database = '${escapeLiteral(dbName)}' AND engine NOT IN ('View', 'MaterializedView') ORDER BY name`;
case 'tdengine':
return `SHOW TABLES FROM \`${dbName.replace(/`/g, '``')}\``;
case 'iotdb': {
const pattern = iotdbDevicePattern(dbName);
return pattern ? `SHOW DEVICES ${pattern}` : 'SHOW DEVICES';
}
case 'dm':
case 'oracle': {
const owner = (schemaName || dbName).toUpperCase();
@@ -219,7 +228,7 @@ const parseTableStats = (dialect: string, rows: Record<string, any>[]): TableSta
};
return {
name: strVal(['Name', 'name', 'table_name', 'tablename', 'TABLE_NAME']),
name: strVal(['Name', 'name', 'table_name', 'tablename', 'TABLE_NAME', 'Device', 'device']),
comment: strVal(['Comment', 'table_comment', 'TABLE_COMMENT', 'comments']),
rows: numVal(['Rows', 'table_rows', 'TABLE_ROWS', 'num_rows', 'reltuples', 'total_rows']),
dataSize: numVal(['Data_length', 'data_length', 'DATA_LENGTH', 'total_bytes']),
@@ -263,6 +272,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
[connection?.config?.driver, connection?.config?.oceanBaseProtocol, connection?.config?.type]
);
const schemaName = String((tab as any).schemaName || '').trim();
const supportsDesignWrite = metadataDialect !== 'iotdb';
const autoFetchVisible = useAutoFetchVisibility();
const loadData = useCallback(async () => {
@@ -415,17 +425,18 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
const openDesign = useCallback((tableName: string) => {
if (!connection) return;
setActiveContext({ connectionId: connection.id, dbName: tab.dbName || '' });
const structureOnly = !supportsDesignWrite;
addTab({
id: `design-${connection.id}-${tab.dbName}-${tableName}`,
title: `设计表 (${tableName})`,
title: `${structureOnly ? '表结构' : '设计表'} (${tableName})`,
type: 'design',
connectionId: connection.id,
dbName: tab.dbName,
tableName,
initialTab: 'columns',
readOnly: false,
readOnly: structureOnly,
});
}, [connection, tab.dbName, addTab, setActiveContext]);
}, [connection, tab.dbName, addTab, setActiveContext, supportsDesignWrite]);
const openTableDdl = useCallback((tableName: string) => {
if (!connection) return;
@@ -827,7 +838,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
const buildLegacyTableContextMenuItems = useCallback((table: TableStatRow): MenuProps['items'] => [
{ key: 'new-query', label: '新建查询', icon: <ConsoleSqlOutlined />, onClick: () => openQueryForTable(table.name) },
{ type: 'divider' },
{ key: 'design-table', label: '设计表', icon: <EditOutlined />, onClick: () => openDesign(table.name) },
{ key: 'design-table', label: supportsDesignWrite ? '设计表' : '表结构', icon: <EditOutlined />, onClick: () => openDesign(table.name) },
{ key: 'copy-table-name', label: '复制表名', icon: <CopyOutlined />, onClick: () => handleCopyTableName(table.name) },
{ key: 'copy-structure', label: '复制表结构', icon: <CopyOutlined />, onClick: () => handleCopyStructure(table.name) },
{ key: 'backup-table', label: '备份表 (SQL)', icon: <SaveOutlined />, onClick: () => handleExport(table.name, 'sql') },
@@ -855,6 +866,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
handleTableDataDangerAction,
openDesign,
openQueryForTable,
supportsDesignWrite,
]);
const renderOverviewSectionTitle = (section: OverviewTableSection) => (