🐛 fix(oceanbase): 修复 OceanBase 协议模式识别与缓存隔离

- 支持 MySQL/Oracle 租户协议在前后端统一解析
- 拒绝 Native 协议并避免误回退为 MySQL
- 修复 Oracle 模式下元数据、DDL、SQL 方言识别
- 修复连接缓存键与实际协议解析优先级不一致问题
- 补充前后端协议解析与缓存隔离回归测试
This commit is contained in:
Syngnat
2026-05-13 22:51:01 +08:00
parent 01eb2c25e0
commit f8abe60dc2
22 changed files with 454 additions and 192 deletions

View File

@@ -96,7 +96,7 @@ const CONNECTION_CONFIG_SECTION_COPY: Record<
},
oceanBaseProtocol: {
title: 'OceanBase 协议',
description: '明确选择 MySQL 租户协议或 Oracle 租户协议。',
description: '明确选择 MySQL 或 Oracle 租户兼容协议。',
},
mongoDiscovery: {
title: 'MongoDB 寻址',

View File

@@ -110,6 +110,31 @@ describe('buildRpcConnectionConfig', () => {
expect(result.connectionParams).toBe('protocol=mysql');
});
it('rejects unsupported OceanBase native protocol instead of falling back to MySQL', () => {
expect(() => buildRpcConnectionConfig({
id: 'conn-oceanbase-native',
type: 'oceanbase',
host: 'ob.local',
port: 2881,
user: 'root@test',
database: 'app',
connectionParams: 'protocol=native',
} as any)).toThrow(/不支持.*native/);
});
it('rejects unsupported OceanBase protocol even when form protocol is explicit MySQL', () => {
expect(() => buildRpcConnectionConfig({
id: 'conn-oceanbase-native-masked',
type: 'oceanbase',
host: 'ob.local',
port: 2881,
user: 'root@test',
database: 'app',
oceanBaseProtocol: 'mysql',
connectionParams: 'protocol=native',
} as any)).toThrow(/不支持.*native/);
});
it('preserves extra connection params for RPC calls', () => {
const result = buildRpcConnectionConfig({
id: 'conn-mysql',

View File

@@ -1,4 +1,8 @@
import { connection } from '../../wailsjs/go/models';
import {
OCEANBASE_PROTOCOL_PARAM_KEYS,
resolveOceanBaseProtocolFromConfig,
} from './oceanBaseProtocol';
export type RpcConnectionConfig = connection.ConnectionConfig & { id?: string };
type ConnectionConfigInput = {
@@ -11,15 +15,6 @@ 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') {
@@ -79,59 +74,12 @@ 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 selectedProtocol = resolveOceanBaseProtocolFromConfig(config);
const params = new URLSearchParams(toStringValue(config.connectionParams));
for (const key of OCEANBASE_PROTOCOL_PARAM_KEYS) {
params.delete(key);

View File

@@ -40,4 +40,16 @@ describe('dataSourceCapabilities', () => {
supportsApproximateTableCount: true,
});
});
it('treats custom OceanBase Oracle driver as Oracle capabilities', () => {
expect(getDataSourceCapabilities({
type: 'custom',
driver: 'oceanbase',
oceanBaseProtocol: 'oracle',
})).toMatchObject({
type: 'oracle',
preferManualTotalCount: true,
supportsApproximateTableCount: true,
});
});
});

View File

@@ -1,4 +1,5 @@
import type { ConnectionConfig } from '../types';
import { normalizeOceanBaseProtocol } from './oceanBaseProtocol';
type ConnectionLike = Pick<ConnectionConfig, 'type' | 'driver' | 'oceanBaseProtocol'> | null | undefined;
@@ -25,9 +26,12 @@ export const resolveDataSourceType = (config: ConnectionLike): string => {
const type = normalizeDataSourceToken(String(config.type || ''));
if (type === 'custom') {
const driver = normalizeDataSourceToken(String(config.driver || ''));
if (driver === 'oceanbase' && normalizeOceanBaseProtocol(config.oceanBaseProtocol) === 'oracle') {
return 'oracle';
}
return driver || 'custom';
}
if (type === 'oceanbase' && String(config.oceanBaseProtocol || '').trim().toLowerCase() === 'oracle') {
if (type === 'oceanbase' && normalizeOceanBaseProtocol(config.oceanBaseProtocol) === 'oracle') {
return 'oracle';
}
return type;

View File

@@ -0,0 +1,109 @@
export type OceanBaseProtocol = 'mysql' | 'oracle';
export const OCEANBASE_PROTOCOL_PARAM_KEYS = [
'protocol',
'oceanBaseProtocol',
'oceanbaseProtocol',
'tenantMode',
'compatMode',
'mode',
];
type OceanBaseProtocolResolution = {
protocol?: OceanBaseProtocol;
unsupportedValue?: string;
unsupportedKey?: string;
};
const normalizeToken = (value: unknown): string => String(value ?? '').trim().toLowerCase();
export const normalizeOceanBaseProtocol = (value: unknown): OceanBaseProtocol | undefined => {
const normalized = normalizeToken(value);
if (!normalized) {
return undefined;
}
if (normalized === 'oracle' || normalized === 'oracle-mode' || normalized === 'oracle_mode' || normalized === 'oboracle') {
return 'oracle';
}
if (normalized === 'mysql' || normalized === 'mysql-compatible' || normalized === 'mysql_compatible' || normalized === 'mysql-mode' || normalized === 'mysql_mode' || normalized === 'obmysql') {
return 'mysql';
}
return undefined;
};
export const isUnsupportedOceanBaseProtocolValue = (value: unknown): boolean => {
const normalized = normalizeToken(value);
return normalized !== '' && !normalizeOceanBaseProtocol(normalized);
};
export const describeUnsupportedOceanBaseProtocol = (value: unknown): string => {
const raw = String(value ?? '').trim();
const label = raw ? ` "${raw}"` : '';
return `OceanBase 当前仅支持 MySQL/Oracle 租户协议,不支持${label};请改为 MySQL 或 Oracle。`;
};
export const resolveOceanBaseProtocolFromQueryText = (raw: unknown): OceanBaseProtocolResolution => {
let text = String(raw ?? '').trim();
if (!text) {
return {};
}
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 value = params.get(key);
if (value == null || String(value).trim() === '') {
continue;
}
const protocol = normalizeOceanBaseProtocol(value);
if (protocol) {
return { protocol };
}
return { unsupportedValue: value, unsupportedKey: key };
}
return {};
};
export const resolveOceanBaseProtocolFromConfig = (config: Record<string, unknown>): OceanBaseProtocol => {
const paramsProtocol = resolveOceanBaseProtocolFromQueryText(config.connectionParams);
const uriProtocol = resolveOceanBaseProtocolFromQueryText(config.uri);
if (Object.prototype.hasOwnProperty.call(config, 'oceanBaseProtocol')) {
const value = config.oceanBaseProtocol;
const protocol = normalizeOceanBaseProtocol(value);
if (isUnsupportedOceanBaseProtocolValue(value)) {
throw new Error(describeUnsupportedOceanBaseProtocol(value));
}
if (paramsProtocol.unsupportedValue) {
throw new Error(describeUnsupportedOceanBaseProtocol(paramsProtocol.unsupportedValue));
}
if (uriProtocol.unsupportedValue) {
throw new Error(describeUnsupportedOceanBaseProtocol(uriProtocol.unsupportedValue));
}
if (protocol) {
return protocol;
}
}
if (paramsProtocol.unsupportedValue) {
throw new Error(describeUnsupportedOceanBaseProtocol(paramsProtocol.unsupportedValue));
}
if (paramsProtocol.protocol) {
return paramsProtocol.protocol;
}
if (uriProtocol.unsupportedValue) {
throw new Error(describeUnsupportedOceanBaseProtocol(uriProtocol.unsupportedValue));
}
return uriProtocol.protocol || 'mysql';
};
export const resolveOceanBaseProtocolForDialect = (value: unknown): OceanBaseProtocol => (
normalizeOceanBaseProtocol(value) || 'mysql'
);

View File

@@ -1,3 +1,5 @@
import { normalizeOceanBaseProtocol } from './oceanBaseProtocol';
const splitQualifiedName = (qualifiedName: string): { schemaName: string; objectName: string } => {
const raw = String(qualifiedName || '').trim();
if (!raw) return { schemaName: '', objectName: '' };
@@ -18,12 +20,14 @@ const normalizeSidebarConnectionDialect = (type: string, driver: string, oceanBa
if (normalizedDriver === 'postgresql' || normalizedDriver === 'postgres' || normalizedDriver === 'pg') return 'postgres';
if (normalizedDriver === 'opengauss' || normalizedDriver === 'open_gauss' || normalizedDriver === 'open-gauss') return 'opengauss';
if (normalizedDriver === 'dameng' || normalizedDriver === 'dm' || normalizedDriver === 'dm8') return 'dm';
if (normalizedDriver === 'oceanbase') return 'mysql';
if (normalizedDriver === 'oceanbase') {
return normalizeOceanBaseProtocol(oceanBaseProtocol) === 'oracle' ? 'oracle' : 'mysql';
}
if (normalizedDriver.includes('oracle')) return 'oracle';
return normalizedDriver;
}
if (normalizedType === 'oceanbase') {
return String(oceanBaseProtocol || '').trim().toLowerCase() === 'oracle' ? 'oracle' : 'mysql';
return normalizeOceanBaseProtocol(oceanBaseProtocol) === 'oracle' ? 'oracle' : 'mysql';
}
if (normalizedType === 'open_gauss' || normalizedType === 'open-gauss') return 'opengauss';
if (normalizedType === 'dameng') return 'dm';

View File

@@ -23,6 +23,7 @@ describe('sqlDialect', () => {
expect(resolveSqlDialect('custom', 'mariadb')).toBe('mariadb');
expect(resolveSqlDialect('custom', 'open_gauss')).toBe('opengauss');
expect(resolveSqlDialect('OceanBase', '', { oceanBaseProtocol: 'oracle' })).toBe('oracle');
expect(resolveSqlDialect('custom', 'oceanbase', { oceanBaseProtocol: 'oracle' })).toBe('oracle');
expect(isMysqlFamilyDialect('mariadb')).toBe(true);
expect(isMysqlFamilyDialect('oceanbase')).toBe(true);
expect(isMysqlFamilyDialect('oracle')).toBe(false);

View File

@@ -1,3 +1,5 @@
import { resolveOceanBaseProtocolForDialect } from './oceanBaseProtocol';
export type ColumnTypeOption = { value: string };
export type SqlFunctionCompletion = {
@@ -34,9 +36,7 @@ const optionValues = (values: string[]): ColumnTypeOption[] => values.map((value
const normalizeRawDialect = (value: string): string => String(value || '').trim().toLowerCase();
export const normalizeOceanBaseSqlProtocol = (value: unknown): 'mysql' | 'oracle' => (
String(value || '').trim().toLowerCase() === 'oracle' ? 'oracle' : 'mysql'
);
export const normalizeOceanBaseSqlProtocol = resolveOceanBaseProtocolForDialect;
export const resolveSqlDialect = (
rawType: string,