mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-09 07:59:33 +08:00
♻️ refactor(oceanbase): 完善双协议连接链路
- 抽象 OceanBase 协议解析与运行态参数注入 - 复用 OracleDB 实现 OceanBase Oracle 租户连接能力 - 调整 DDL、schema、SQL 方言和数据源能力判断 - 补充协议优先级、缓存隔离和 RPC 参数测试 - 支持按指定 driver 自动生成 agent revision
This commit is contained in:
@@ -21,6 +21,7 @@ export type ConnectionConfigSectionKey =
|
||||
| 'target'
|
||||
| 'fileTarget'
|
||||
| 'connectionMode'
|
||||
| 'oceanBaseProtocol'
|
||||
| 'mongoDiscovery'
|
||||
| 'replica'
|
||||
| 'service'
|
||||
@@ -93,6 +94,10 @@ const CONNECTION_CONFIG_SECTION_COPY: Record<
|
||||
title: '连接模式',
|
||||
description: '选择单机、主从、副本集或集群等拓扑模式。',
|
||||
},
|
||||
oceanBaseProtocol: {
|
||||
title: 'OceanBase 协议',
|
||||
description: '明确选择 MySQL 租户协议或 Oracle 租户协议。',
|
||||
},
|
||||
mongoDiscovery: {
|
||||
title: 'MongoDB 寻址',
|
||||
description: '选择标准 host:port 或 mongodb+srv DNS 发现方式。',
|
||||
|
||||
@@ -52,6 +52,64 @@ describe('buildRpcConnectionConfig', () => {
|
||||
expect(result.clickHouseProtocol).toBe('http');
|
||||
});
|
||||
|
||||
it('injects OceanBase protocol override into RPC connection params', () => {
|
||||
const result = buildRpcConnectionConfig({
|
||||
id: 'conn-oceanbase-oracle',
|
||||
type: 'oceanbase',
|
||||
host: 'ob.local',
|
||||
port: 2881,
|
||||
user: 'sys@oracle001',
|
||||
database: 'ORCL',
|
||||
oceanBaseProtocol: 'oracle',
|
||||
} as any);
|
||||
|
||||
expect(result.connectionParams).toBe('protocol=oracle');
|
||||
expect((result as any).oceanBaseProtocol).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps OceanBase URI protocol when no form override exists', () => {
|
||||
const result = buildRpcConnectionConfig({
|
||||
id: 'conn-oceanbase-uri',
|
||||
type: 'oceanbase',
|
||||
host: 'ob.local',
|
||||
port: 2881,
|
||||
user: 'sys@oracle001',
|
||||
database: 'ORCL',
|
||||
uri: 'oceanbase://sys%40oracle001:pass@ob.local:2881/ORCL?protocol=oracle',
|
||||
} as any);
|
||||
|
||||
expect(result.connectionParams).toBe('protocol=oracle');
|
||||
});
|
||||
|
||||
it('lets OceanBase form protocol override legacy connection param aliases', () => {
|
||||
const result = buildRpcConnectionConfig({
|
||||
id: 'conn-oceanbase-mysql',
|
||||
type: 'oceanbase',
|
||||
host: 'ob.local',
|
||||
port: 2881,
|
||||
user: 'root@test',
|
||||
database: 'app',
|
||||
oceanBaseProtocol: 'mysql',
|
||||
connectionParams: 'tenantMode=oracle&connectTimeout=10',
|
||||
} as any);
|
||||
|
||||
expect(result.connectionParams).toBe('connectTimeout=10&protocol=mysql');
|
||||
});
|
||||
|
||||
it('keeps OceanBase protocol query key ahead of compatibility aliases', () => {
|
||||
const result = buildRpcConnectionConfig({
|
||||
id: 'conn-oceanbase-conflict',
|
||||
type: 'oceanbase',
|
||||
host: 'ob.local',
|
||||
port: 2881,
|
||||
user: 'root@test',
|
||||
database: 'app',
|
||||
connectionParams: 'protocol=mysql&tenantMode=oracle',
|
||||
} as any);
|
||||
|
||||
expect(result.connectionParams).toBe('protocol=mysql');
|
||||
});
|
||||
|
||||
it('preserves extra connection params for RPC calls', () => {
|
||||
const result = buildRpcConnectionConfig({
|
||||
id: 'conn-mysql',
|
||||
|
||||
@@ -11,6 +11,15 @@ type ConnectionConfigInput = {
|
||||
type SSHConfigInput = Record<string, any>;
|
||||
type ProxyConfigInput = Record<string, any>;
|
||||
type HttpTunnelConfigInput = Record<string, any>;
|
||||
type OceanBaseProtocol = 'mysql' | 'oracle';
|
||||
const OCEANBASE_PROTOCOL_PARAM_KEYS = [
|
||||
'protocol',
|
||||
'oceanBaseProtocol',
|
||||
'oceanbaseProtocol',
|
||||
'tenantMode',
|
||||
'compatMode',
|
||||
'mode',
|
||||
];
|
||||
|
||||
const toStringValue = (value: unknown, fallback = ''): string => {
|
||||
if (typeof value === 'string') {
|
||||
@@ -70,6 +79,70 @@ const normalizeHttpTunnelConfig = (value: unknown): connection.HTTPTunnelConfig
|
||||
});
|
||||
};
|
||||
|
||||
const normalizeOceanBaseProtocol = (value: unknown): OceanBaseProtocol | undefined => {
|
||||
const normalized = toStringValue(value).trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
return normalized === 'oracle' || normalized === 'oracle-mode' || normalized === 'oracle_mode' || normalized === 'oboracle'
|
||||
? 'oracle'
|
||||
: 'mysql';
|
||||
};
|
||||
|
||||
const resolveOceanBaseProtocolFromQueryText = (raw: unknown): OceanBaseProtocol | undefined => {
|
||||
let text = toStringValue(raw).trim();
|
||||
if (!text) {
|
||||
return undefined;
|
||||
}
|
||||
const queryStart = text.indexOf('?');
|
||||
if (queryStart >= 0) {
|
||||
text = text.slice(queryStart + 1);
|
||||
}
|
||||
const hashStart = text.indexOf('#');
|
||||
if (hashStart >= 0) {
|
||||
text = text.slice(0, hashStart);
|
||||
}
|
||||
const params = new URLSearchParams(text.replace(/^[?&]+/, ''));
|
||||
for (const key of OCEANBASE_PROTOCOL_PARAM_KEYS) {
|
||||
const protocol = normalizeOceanBaseProtocol(params.get(key));
|
||||
if (protocol) {
|
||||
return protocol;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const resolveOceanBaseProtocol = (config: ConnectionConfigInput): OceanBaseProtocol => {
|
||||
if (Object.prototype.hasOwnProperty.call(config, 'oceanBaseProtocol')) {
|
||||
const explicitProtocol = normalizeOceanBaseProtocol(config.oceanBaseProtocol);
|
||||
if (explicitProtocol) {
|
||||
return explicitProtocol;
|
||||
}
|
||||
}
|
||||
return (
|
||||
resolveOceanBaseProtocolFromQueryText(config.connectionParams) ||
|
||||
resolveOceanBaseProtocolFromQueryText(config.uri) ||
|
||||
'mysql'
|
||||
);
|
||||
};
|
||||
|
||||
const withOceanBaseProtocolParam = (config: ConnectionConfigInput): ConnectionConfigInput => {
|
||||
const type = toStringValue(config.type).trim().toLowerCase();
|
||||
if (type !== 'oceanbase') {
|
||||
return config;
|
||||
}
|
||||
const selectedProtocol = resolveOceanBaseProtocol(config);
|
||||
const params = new URLSearchParams(toStringValue(config.connectionParams));
|
||||
for (const key of OCEANBASE_PROTOCOL_PARAM_KEYS) {
|
||||
params.delete(key);
|
||||
}
|
||||
params.set('protocol', selectedProtocol);
|
||||
return {
|
||||
...config,
|
||||
connectionParams: params.toString(),
|
||||
};
|
||||
};
|
||||
|
||||
export function buildRpcConnectionConfig(
|
||||
config: ConnectionConfigInput,
|
||||
overrides: ConnectionConfigInput = {},
|
||||
@@ -93,25 +166,26 @@ export function buildRpcConnectionConfig(
|
||||
proxy: mergedProxy,
|
||||
httpTunnel: mergedHttpTunnel,
|
||||
};
|
||||
const rpcMerged = withOceanBaseProtocolParam(merged);
|
||||
|
||||
const baseId = toStringValue(config.id).trim() || toStringValue(overrides.id).trim() || undefined;
|
||||
const timeout = toOptionalInteger(merged.timeout, toOptionalInteger(config.timeout));
|
||||
const redisDB = toOptionalInteger(merged.redisDB, toOptionalInteger(config.redisDB));
|
||||
const timeout = toOptionalInteger(rpcMerged.timeout, toOptionalInteger(config.timeout));
|
||||
const redisDB = toOptionalInteger(rpcMerged.redisDB, toOptionalInteger(config.redisDB));
|
||||
|
||||
const rpcConfig = new connection.ConnectionConfig({
|
||||
...merged,
|
||||
type: toStringValue(merged.type),
|
||||
host: toStringValue(merged.host),
|
||||
port: toOptionalInteger(merged.port, toOptionalInteger(config.port, 0)) ?? 0,
|
||||
user: toStringValue(merged.user),
|
||||
password: toStringValue(merged.password),
|
||||
database: toStringValue(merged.database),
|
||||
useSSH: merged.useSSH === true,
|
||||
ssh: normalizeSSHConfig(merged.ssh),
|
||||
useProxy: merged.useProxy === true,
|
||||
proxy: normalizeProxyConfig(merged.proxy),
|
||||
useHttpTunnel: merged.useHttpTunnel === true,
|
||||
httpTunnel: normalizeHttpTunnelConfig(merged.httpTunnel),
|
||||
...rpcMerged,
|
||||
type: toStringValue(rpcMerged.type),
|
||||
host: toStringValue(rpcMerged.host),
|
||||
port: toOptionalInteger(rpcMerged.port, toOptionalInteger(config.port, 0)) ?? 0,
|
||||
user: toStringValue(rpcMerged.user),
|
||||
password: toStringValue(rpcMerged.password),
|
||||
database: toStringValue(rpcMerged.database),
|
||||
useSSH: rpcMerged.useSSH === true,
|
||||
ssh: normalizeSSHConfig(rpcMerged.ssh),
|
||||
useProxy: rpcMerged.useProxy === true,
|
||||
proxy: normalizeProxyConfig(rpcMerged.proxy),
|
||||
useHttpTunnel: rpcMerged.useHttpTunnel === true,
|
||||
httpTunnel: normalizeHttpTunnelConfig(rpcMerged.httpTunnel),
|
||||
timeout,
|
||||
redisDB,
|
||||
}) as RpcConnectionConfig;
|
||||
@@ -119,4 +193,3 @@ export function buildRpcConnectionConfig(
|
||||
rpcConfig.id = baseId;
|
||||
return rpcConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,4 +29,15 @@ describe('dataSourceCapabilities', () => {
|
||||
supportsApproximateTotalPages: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats OceanBase Oracle protocol as Oracle capabilities', () => {
|
||||
expect(getDataSourceCapabilities({
|
||||
type: 'oceanbase',
|
||||
oceanBaseProtocol: 'oracle',
|
||||
})).toMatchObject({
|
||||
type: 'oracle',
|
||||
preferManualTotalCount: true,
|
||||
supportsApproximateTableCount: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ConnectionConfig } from '../types';
|
||||
|
||||
type ConnectionLike = Pick<ConnectionConfig, 'type' | 'driver'> | null | undefined;
|
||||
type ConnectionLike = Pick<ConnectionConfig, 'type' | 'driver' | 'oceanBaseProtocol'> | null | undefined;
|
||||
|
||||
const normalizeDataSourceToken = (raw: string): string => {
|
||||
const normalized = String(raw || '').trim().toLowerCase();
|
||||
@@ -27,6 +27,9 @@ export const resolveDataSourceType = (config: ConnectionLike): string => {
|
||||
const driver = normalizeDataSourceToken(String(config.driver || ''));
|
||||
return driver || 'custom';
|
||||
}
|
||||
if (type === 'oceanbase' && String(config.oceanBaseProtocol || '').trim().toLowerCase() === 'oracle') {
|
||||
return 'oracle';
|
||||
}
|
||||
return type;
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const splitQualifiedName = (qualifiedName: string): { schemaName: string; object
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeSidebarConnectionDialect = (type: string, driver: string): string => {
|
||||
const normalizeSidebarConnectionDialect = (type: string, driver: string, oceanBaseProtocol?: string): string => {
|
||||
const normalizedType = String(type || '').trim().toLowerCase();
|
||||
if (normalizedType === 'custom') {
|
||||
const normalizedDriver = String(driver || '').trim().toLowerCase();
|
||||
@@ -22,7 +22,9 @@ const normalizeSidebarConnectionDialect = (type: string, driver: string): string
|
||||
if (normalizedDriver.includes('oracle')) return 'oracle';
|
||||
return normalizedDriver;
|
||||
}
|
||||
if (normalizedType === 'oceanbase') return 'mysql';
|
||||
if (normalizedType === 'oceanbase') {
|
||||
return String(oceanBaseProtocol || '').trim().toLowerCase() === 'oracle' ? 'oracle' : 'mysql';
|
||||
}
|
||||
if (normalizedType === 'open_gauss' || normalizedType === 'open-gauss') return 'opengauss';
|
||||
if (normalizedType === 'dameng') return 'dm';
|
||||
return normalizedType;
|
||||
@@ -59,6 +61,7 @@ export const resolveSidebarRuntimeDatabase = (
|
||||
savedDatabase: string,
|
||||
overrideDatabase?: string,
|
||||
clearDatabase: boolean = false,
|
||||
oceanBaseProtocol?: string,
|
||||
): string => {
|
||||
if (clearDatabase) return '';
|
||||
|
||||
@@ -68,7 +71,7 @@ export const resolveSidebarRuntimeDatabase = (
|
||||
return normalizedSavedDatabase;
|
||||
}
|
||||
|
||||
const dialect = normalizeSidebarConnectionDialect(type, driver);
|
||||
const dialect = normalizeSidebarConnectionDialect(type, driver, oceanBaseProtocol);
|
||||
if (dialect === 'oracle' || dialect === 'dm') {
|
||||
return normalizedSavedDatabase || normalizedOverrideDatabase;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ describe('sqlDialect', () => {
|
||||
expect(resolveSqlDialect('custom', 'dm8')).toBe('dameng');
|
||||
expect(resolveSqlDialect('custom', 'mariadb')).toBe('mariadb');
|
||||
expect(resolveSqlDialect('custom', 'open_gauss')).toBe('opengauss');
|
||||
expect(resolveSqlDialect('OceanBase', '', { oceanBaseProtocol: 'oracle' })).toBe('oracle');
|
||||
expect(isMysqlFamilyDialect('mariadb')).toBe(true);
|
||||
expect(isMysqlFamilyDialect('oceanbase')).toBe(true);
|
||||
expect(isMysqlFamilyDialect('oracle')).toBe(false);
|
||||
|
||||
@@ -34,12 +34,23 @@ const optionValues = (values: string[]): ColumnTypeOption[] => values.map((value
|
||||
|
||||
const normalizeRawDialect = (value: string): string => String(value || '').trim().toLowerCase();
|
||||
|
||||
export const resolveSqlDialect = (rawType: string, rawDriver = ''): SqlDialect => {
|
||||
export const normalizeOceanBaseSqlProtocol = (value: unknown): 'mysql' | 'oracle' => (
|
||||
String(value || '').trim().toLowerCase() === 'oracle' ? 'oracle' : 'mysql'
|
||||
);
|
||||
|
||||
export const resolveSqlDialect = (
|
||||
rawType: string,
|
||||
rawDriver = '',
|
||||
options?: { oceanBaseProtocol?: unknown },
|
||||
): SqlDialect => {
|
||||
const normalized = normalizeRawDialect(rawType);
|
||||
const driver = normalizeRawDialect(rawDriver);
|
||||
const source = normalized === 'custom' ? driver : normalized;
|
||||
|
||||
if (!source) return 'unknown';
|
||||
if (source === 'oceanbase' && normalizeOceanBaseSqlProtocol(options?.oceanBaseProtocol) === 'oracle') {
|
||||
return 'oracle';
|
||||
}
|
||||
|
||||
switch (source) {
|
||||
case 'postgresql':
|
||||
|
||||
Reference in New Issue
Block a user