feat(gaussdb): 新增 GaussDB 数据库连接支持

Refs #497
This commit is contained in:
Syngnat
2026-06-13 19:34:52 +08:00
parent f3dfffb8d1
commit d2f68acae8
70 changed files with 717 additions and 73 deletions

View File

@@ -76,6 +76,19 @@ describe('ConnectionModal data source registry', () => {
expect(source).toContain('return "iotdb://root:root@127.0.0.1:6667/root.sg";');
expect(source).toContain('return "fetchSize=1024&timeZone=Asia%2FShanghai";');
});
it('exposes GaussDB in the create-connection picker with PostgreSQL-family defaults', () => {
expect(source).toContain("case 'gaussdb':");
expect(source).toContain('return 5432;');
expect(source).toContain('gaussdb: ["gaussdb", "postgresql", "postgres"]');
expect(source).toContain("key: 'gaussdb'");
expect(source).toContain("name: 'GaussDB'");
expect(source).toContain('type === "gaussdb"');
expect(source).toContain('return "gaussdb://user:pass@127.0.0.1:5432/db_name";');
expect(source).toContain('return "application_name=GoNavi&statement_timeout=30000";');
expect(source).toContain('? "gaussdb"');
expect(source).toContain('dbType === "gaussdb"');
});
});
describe('ConnectionModal Redis Sentinel configuration', () => {

View File

@@ -1676,7 +1676,8 @@ const ConnectionModal: React.FC<{
type === "kingbase" ||
type === "highgo" ||
type === "vastbase" ||
type === "opengauss"
type === "opengauss" ||
type === "gaussdb"
) {
const sslMode = String(parsed.params.get("sslmode") || "")
.trim()
@@ -1888,6 +1889,9 @@ const ConnectionModal: React.FC<{
if (dbType === "opengauss") {
return "opengauss://user:pass@127.0.0.1:5432/db_name";
}
if (dbType === "gaussdb") {
return "gaussdb://user:pass@127.0.0.1:5432/db_name";
}
return "例如: postgres://user:pass@127.0.0.1:5432/db_name";
};
@@ -1906,6 +1910,7 @@ const ConnectionModal: React.FC<{
case "highgo":
case "vastbase":
case "opengauss":
case "gaussdb":
return "application_name=GoNavi&statement_timeout=30000";
case "oracle":
return "PREFETCH_ROWS=5000&TRACE FILE=/tmp/go-ora.trc";
@@ -2062,7 +2067,9 @@ const ConnectionModal: React.FC<{
? normalizeClickHouseProtocolValue(values.clickHouseProtocol)
: "auto";
const scheme =
type === "postgres"
type === "gaussdb"
? "gaussdb"
: type === "postgres"
? "postgresql"
: type === "chroma" || type === "qdrant"
? values.useSSL
@@ -4826,7 +4833,8 @@ const ConnectionModal: React.FC<{
dbType === "kingbase" ||
dbType === "highgo" ||
dbType === "vastbase" ||
dbType === "opengauss") &&
dbType === "opengauss" ||
dbType === "gaussdb") &&
renderConfigSectionCard({
sectionKey: "service",
icon: <DatabaseOutlined />,

View File

@@ -39,6 +39,13 @@ describe('DatabaseIcons', () => {
expect(markup).toContain('>Io</text>');
});
it('includes GaussDB in the selectable database icons', () => {
expect(DB_ICON_TYPES).toContain('gaussdb');
expect(getDbIconLabel('gaussdb')).toBe('GaussDB');
const markup = renderToStaticMarkup(<>{getDbIcon('gaussdb', undefined, 22)}</>);
expect(markup).toContain('>GS</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

@@ -46,6 +46,7 @@ const DB_DEFAULT_COLORS: Record<string, string> = {
duckdb: '#FFC107',
vastbase: '#0066CC',
opengauss: '#2446A8',
gaussdb: '#0B7FAB',
highgo: '#00A86B',
iris: '#1F6FEB',
tdengine: '#2962FF',
@@ -172,6 +173,9 @@ const VastBaseIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
const OpenGaussIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
<ColorBadge size={size} color={color || DB_DEFAULT_COLORS.opengauss} label="OG" />
);
const GaussDBIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
<ColorBadge size={size} color={color || DB_DEFAULT_COLORS.gaussdb} label="GS" />
);
const HighGoIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
<ColorBadge size={size} color={color || DB_DEFAULT_COLORS.highgo} label="HG" />
);
@@ -240,6 +244,7 @@ const DB_ICON_MAP: Record<string, React.FC<DbIconProps>> = {
duckdb: DuckDBIcon,
vastbase: VastBaseIcon,
opengauss: OpenGaussIcon,
gaussdb: GaussDBIcon,
highgo: HighGoIcon,
iris: IrisIcon,
tdengine: TDengineIcon,
@@ -254,7 +259,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', 'iotdb', 'chroma', 'qdrant', 'elasticsearch', 'custom',
'kingbase', 'dameng', 'vastbase', 'opengauss', 'gaussdb', 'highgo', 'iris', 'tdengine', 'iotdb', 'chroma', 'qdrant', 'elasticsearch', 'custom',
];
/** 该类型是否有品牌 SVG 文件 */
@@ -276,7 +281,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', iotdb: 'Apache IoTDB',
vastbase: 'VastBase', opengauss: 'OpenGauss', gaussdb: 'GaussDB', highgo: '瀚高', iris: 'InterSystems IRIS', tdengine: 'TDengine', iotdb: 'Apache IoTDB',
chroma: 'Chroma',
qdrant: 'Qdrant',
elasticsearch: 'Elasticsearch',

View File

@@ -98,6 +98,7 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
if (driver === 'diros' || driver === 'doris') return 'mysql';
if (driver === 'oceanbase') return normalizeOceanBaseProtocol(conn?.config?.oceanBaseProtocol) === 'oracle' ? 'oracle' : 'mysql';
if (driver === 'opengauss' || driver === 'open_gauss' || driver === 'open-gauss') return 'opengauss';
if (driver === 'gaussdb' || driver === 'gauss_db' || driver === 'gauss-db') return 'gaussdb';
return driver;
}
if (type === 'oceanbase' && normalizeOceanBaseProtocol(conn?.config?.oceanBaseProtocol) === 'oracle') return 'oracle';
@@ -196,7 +197,8 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
case 'kingbase':
case 'highgo':
case 'vastbase':
case 'opengauss': {
case 'opengauss':
case 'gaussdb': {
const schemaRef = schema || 'public';
return [`SELECT pg_get_viewdef('${escapeSQLLiteral(schemaRef)}.${safeName}'::regclass, true) AS view_definition`];
}
@@ -244,7 +246,8 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
case 'kingbase':
case 'highgo':
case 'vastbase':
case 'opengauss': {
case 'opengauss':
case 'gaussdb': {
const schemaRef = schema || 'public';
return [`SELECT pg_get_functiondef(p.oid) AS routine_definition FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = '${escapeSQLLiteral(schemaRef)}' AND p.proname = '${safeName}' LIMIT 1`];
}

View File

@@ -4291,6 +4291,7 @@ describe('QueryEditor external SQL save', () => {
'highgo',
'vastbase',
'opengauss',
'gaussdb',
'sqlserver',
'sqlite',
'duckdb',

View File

@@ -189,7 +189,7 @@ const isSystemMetadataQueryResult = (tableRef: QueryResultTableRef, dbType: stri
if (['mysql', 'mariadb', 'oceanbase', 'diros', 'starrocks', 'sphinx', 'tidb'].includes(normalizedDbType)) {
return MYSQL_SYSTEM_METADATA_SCHEMAS.has(metadataDbName);
}
if (['postgres', 'kingbase', 'highgo', 'vastbase', 'opengauss'].includes(normalizedDbType)) {
if (['postgres', 'kingbase', 'highgo', 'vastbase', 'opengauss', 'gaussdb'].includes(normalizedDbType)) {
return POSTGRES_SYSTEM_METADATA_SCHEMAS.has(metadataDbName);
}
if (normalizedDbType === 'sqlite' || normalizedDbType === 'duckdb') {
@@ -479,6 +479,7 @@ const buildCompletionTableCommentSQL = (dialect: string, dbName: string): string
case 'vastbase':
case 'highgo':
case 'opengauss':
case 'gaussdb':
return `SELECT n.nspname || '.' || c.relname AS table_name, obj_description(c.oid, 'pg_class') AS table_comment FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN ('r', 'p') AND n.nspname NOT IN ('pg_catalog', 'information_schema') AND n.nspname NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY n.nspname, c.relname`;
case 'sqlserver': {
const safeDb = quoteSqlServerDbIdentifier(db);
@@ -685,6 +686,7 @@ const buildCompletionViewsMetadataQuerySpecs = (dialect: string, dbName: string)
case 'highgo':
case 'vastbase':
case 'opengauss':
case 'gaussdb':
return [{ sql: `SELECT schemaname AS schema_name, viewname AS view_name FROM pg_catalog.pg_views WHERE schemaname != 'information_schema' AND schemaname NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY schemaname, viewname` }];
case 'sqlserver': {
const safeDb = quoteSqlServerDbIdentifier(dbName || 'master');
@@ -748,6 +750,7 @@ const buildCompletionTriggersMetadataQuerySpecs = (dialect: string, dbName: stri
case 'highgo':
case 'vastbase':
case 'opengauss':
case 'gaussdb':
return [{ sql: `SELECT DISTINCT event_object_schema AS schema_name, event_object_table AS table_name, trigger_name FROM information_schema.triggers WHERE trigger_schema NOT IN ('pg_catalog', 'information_schema') AND trigger_schema NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY event_object_schema, event_object_table, trigger_name` }];
case 'sqlserver': {
const safeDb = quoteSqlServerDbIdentifier(dbName || 'master');
@@ -790,6 +793,7 @@ const buildCompletionFunctionsMetadataQuerySpecs = (dialect: string, dbName: str
case 'highgo':
case 'vastbase':
case 'opengauss':
case 'gaussdb':
return normalizeMetadataQuerySpecs([
{
sql: `SELECT n.nspname AS schema_name, p.proname AS routine_name, CASE WHEN p.prokind = 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END AS routine_type FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') AND n.nspname NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY n.nspname, routine_type, p.proname`,

View File

@@ -1009,6 +1009,7 @@ const Sidebar: React.FC<{
'highgo',
'vastbase',
'opengauss',
'gaussdb',
'open_gauss',
'open-gauss',
'sqlserver',
@@ -1023,6 +1024,7 @@ const Sidebar: React.FC<{
'highgo',
'vastbase',
'opengauss',
'gaussdb',
'open_gauss',
'open-gauss',
'sqlserver',
@@ -1171,6 +1173,7 @@ const Sidebar: React.FC<{
case 'vastbase':
case 'highgo':
case 'opengauss':
case 'gaussdb':
return [
"SELECT n.nspname || '.' || c.relname AS table_name, c.reltuples::bigint AS table_rows",
'FROM pg_class c',
@@ -1292,6 +1295,7 @@ const Sidebar: React.FC<{
case 'highgo':
case 'vastbase':
case 'opengauss':
case 'gaussdb':
return [{ sql: `SELECT schemaname AS schema_name, viewname AS view_name FROM pg_catalog.pg_views WHERE schemaname != 'information_schema' AND schemaname NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY schemaname, viewname` }];
case 'sqlserver': {
const safeDb = quoteSqlServerIdentifier(dbName || 'master');
@@ -1338,6 +1342,7 @@ const Sidebar: React.FC<{
case 'highgo':
case 'vastbase':
case 'opengauss':
case 'gaussdb':
return [{ sql: `SELECT DISTINCT event_object_schema AS schema_name, event_object_table AS table_name, trigger_name FROM information_schema.triggers WHERE trigger_schema NOT IN ('pg_catalog', 'information_schema') AND trigger_schema NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY event_object_schema, event_object_table, trigger_name` }];
case 'sqlserver': {
const safeDb = quoteSqlServerIdentifier(dbName || 'master');
@@ -1387,6 +1392,7 @@ const Sidebar: React.FC<{
case 'highgo':
case 'vastbase':
case 'opengauss':
case 'gaussdb':
return normalizeMetadataQuerySpecs([
{
// PostgreSQL 11+ / 部分 PG-like通过 prokind 区分 FUNCTION/PROCEDURE
@@ -4299,7 +4305,7 @@ const Sidebar: React.FC<{
case 'starrocks':
query = `SHOW CREATE VIEW \`${viewName.replace(/`/g, '``')}\``;
break;
case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': case 'opengauss': {
case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': case 'opengauss': case 'gaussdb': {
const parts = splitQualifiedName(viewName);
const schema = parts.schemaName || 'public';
const name = parts.objectName || viewName;
@@ -4356,7 +4362,7 @@ const Sidebar: React.FC<{
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':
case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': case 'opengauss': case 'gaussdb':
template = `CREATE OR REPLACE VIEW view_name AS\nSELECT column1, column2\nFROM table_name\nWHERE condition;`;
break;
case 'sqlserver':
@@ -4583,7 +4589,7 @@ const Sidebar: React.FC<{
case 'starrocks':
query = `SHOW CREATE ${routineType} \`${name.replace(/`/g, '``')}\``;
break;
case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': case 'opengauss': {
case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': case 'opengauss': case 'gaussdb': {
const schemaRef = schema || 'public';
query = `SELECT pg_get_functiondef(p.oid) AS routine_definition FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = '${escapeSQLLiteral(schemaRef)}' AND p.proname = '${escapeSQLLiteral(name)}' LIMIT 1`;
break;
@@ -4654,7 +4660,7 @@ const Sidebar: React.FC<{
? `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 ;`;
break;
case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': case 'opengauss':
case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': case 'opengauss': case 'gaussdb':
template = isProc
? `CREATE OR REPLACE PROCEDURE proc_name(param1 integer)\nLANGUAGE plpgsql\nAS $$\nBEGIN\n -- procedure body\nEND;\n$$;`
: `CREATE OR REPLACE FUNCTION func_name(param1 integer)\nRETURNS integer\nLANGUAGE plpgsql\nAS $$\nBEGIN\n RETURN param1 * 2;\nEND;\n$$;`;
@@ -5655,6 +5661,7 @@ const Sidebar: React.FC<{
const iconType = resolveConnectionIconType(conn);
if (iconType === 'mysql' || iconType === 'mariadb' || iconType === 'oceanbase') return 'MY';
if (iconType === 'postgres') return 'PG';
if (iconType === 'gaussdb') return 'GS';
if (iconType === 'redis') return 'R';
if (iconType === 'mongodb') return 'MO';
if (iconType === 'oracle') return 'OR';
@@ -5810,7 +5817,8 @@ const Sidebar: React.FC<{
case 'kingbase':
case 'vastbase':
case 'highgo':
case 'opengauss': {
case 'opengauss':
case 'gaussdb': {
const schema = schemaName || 'public';
return [
"SELECT c.reltuples::bigint AS table_rows, pg_total_relation_size(c.oid) AS data_length, pg_indexes_size(c.oid) AS index_length, 'heap' AS engine",

View File

@@ -930,6 +930,7 @@ const TableDesigner: React.FC<{ tab: TabData; embedded?: boolean }> = ({ tab, em
if (normalized === 'mssql' || normalized === 'sql_server' || normalized === 'sql-server') return 'sqlserver';
if (normalized === 'doris') return 'diros';
if (normalized === 'open_gauss' || normalized === 'open-gauss') return 'opengauss';
if (normalized === 'gauss_db' || normalized === 'gauss-db') return 'gaussdb';
return normalized;
};
@@ -981,6 +982,7 @@ END;`;
case 'highgo':
case 'vastbase':
case 'opengauss':
case 'gaussdb':
return `CREATE OR REPLACE FUNCTION trigger_function_name()
RETURNS TRIGGER AS $$
BEGIN
@@ -1039,6 +1041,7 @@ END;`;
case 'highgo':
case 'vastbase':
case 'opengauss':
case 'gaussdb':
return `DROP TRIGGER IF EXISTS "${triggerName}" ON "${tblName}"`;
case 'sqlserver':
return `DROP TRIGGER IF EXISTS [${triggerName}]`;

View File

@@ -122,6 +122,7 @@ const getMetadataDialect = (connType: string, driver?: string, oceanBaseProtocol
if (d === 'diros' || d === 'doris') return 'mysql';
if (d === 'oceanbase') return normalizeOceanBaseProtocol(oceanBaseProtocol) === 'oracle' ? 'oracle' : 'mysql';
if (d === 'opengauss' || d === 'open_gauss' || d === 'open-gauss') return 'opengauss';
if (d === 'gaussdb' || d === 'gauss_db' || d === 'gauss-db') return 'gaussdb';
return d;
}
if (type === 'oceanbase' && normalizeOceanBaseProtocol(oceanBaseProtocol) === 'oracle') return 'oracle';
@@ -158,7 +159,8 @@ ORDER BY table_name`;
case 'kingbase':
case 'vastbase':
case 'highgo':
case 'opengauss': {
case 'opengauss':
case 'gaussdb': {
const schema = schemaName || 'public';
return `
SELECT

View File

@@ -138,6 +138,7 @@ const TriggerViewer: React.FC<TriggerViewerProps> = ({ tab }) => {
if (driver === 'diros' || driver === 'doris') return 'mysql';
if (driver === 'oceanbase') return normalizeOceanBaseProtocol(conn?.config?.oceanBaseProtocol) === 'oracle' ? 'oracle' : 'mysql';
if (driver === 'opengauss' || driver === 'open_gauss' || driver === 'open-gauss') return 'opengauss';
if (driver === 'gaussdb' || driver === 'gauss_db' || driver === 'gauss-db') return 'gaussdb';
return driver;
}
if (type === 'oceanbase' && normalizeOceanBaseProtocol(conn?.config?.oceanBaseProtocol) === 'oracle') return 'oracle';
@@ -175,6 +176,7 @@ const TriggerViewer: React.FC<TriggerViewerProps> = ({ tab }) => {
case 'highgo':
case 'vastbase':
case 'opengauss':
case 'gaussdb':
return [`SELECT pg_get_triggerdef(t.oid, true) AS trigger_definition
FROM pg_trigger t
JOIN pg_class c ON t.tgrelid = c.oid
@@ -310,7 +312,8 @@ LIMIT 1`];
case 'kingbase':
case 'highgo':
case 'vastbase':
case 'opengauss': {
case 'opengauss':
case 'gaussdb': {
return row.trigger_definition || row.TRIGGER_DEFINITION || Object.values(row)[0] || '';
}
case 'sqlserver': {

View File

@@ -37,8 +37,10 @@ describe('sidebarCoreUtils', () => {
it('normalizes driver aliases used by sidebar metadata loaders', () => {
expect(normalizeDriverType('postgresql')).toBe('postgres');
expect(normalizeDriverType('open-gauss')).toBe('opengauss');
expect(normalizeDriverType('gauss-db')).toBe('gaussdb');
expect(normalizeDriverType('InterSystemsIRIS')).toBe('iris');
expect(isPostgresSchemaDialect('kingbase')).toBe(true);
expect(isPostgresSchemaDialect('gauss-db')).toBe(true);
});
it('resolves draggable object labels by object kind', () => {

View File

@@ -8,8 +8,10 @@ describe('tableDataDangerActions', () => {
expect(supportsTableTruncateAction('oceanbase')).toBe(true);
expect(supportsTableTruncateAction('postgres')).toBe(true);
expect(supportsTableTruncateAction('opengauss')).toBe(true);
expect(supportsTableTruncateAction('gaussdb')).toBe(true);
expect(supportsTableTruncateAction('iris')).toBe(true);
expect(supportsTableTruncateAction('custom', 'postgresql')).toBe(true);
expect(supportsTableTruncateAction('custom', 'gauss_db')).toBe(true);
expect(supportsTableTruncateAction('custom', 'kingbase8')).toBe(true);
expect(supportsTableTruncateAction('custom', 'intersystemsiris')).toBe(true);
});

View File

@@ -13,6 +13,10 @@ const resolveCustomDriverDialect = (driver: string): string => {
case 'open_gauss':
case 'open-gauss':
return 'opengauss';
case 'gaussdb':
case 'gauss_db':
case 'gauss-db':
return 'gaussdb';
case 'dm':
case 'dameng':
case 'dm8':
@@ -49,6 +53,7 @@ const resolveCustomDriverDialect = (driver: string): string => {
}
if (normalized.includes('opengauss') || normalized.includes('open_gauss') || normalized.includes('open-gauss')) return 'opengauss';
if (normalized.includes('gaussdb') || normalized.includes('gauss_db') || normalized.includes('gauss-db')) return 'gaussdb';
if (normalized.includes('postgres')) return 'postgres';
if (normalized.includes('oceanbase')) return 'oceanbase';
if (normalized.includes('kingbase')) return 'kingbase';
@@ -81,6 +86,7 @@ export const supportsTableTruncateAction = (type: string, driver?: string): bool
case 'highgo':
case 'vastbase':
case 'opengauss':
case 'gaussdb':
case 'sqlserver':
case 'iris':
case 'oracle':