🐛 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

@@ -64,6 +64,13 @@ import { getCustomConnectionDsnValidationMessage } from "../utils/customConnecti
import { mergeParsedUriValuesForForm } from "../utils/connectionUriMerge";
import { buildRpcConnectionConfig } from "../utils/connectionRpcConfig";
import { CUSTOM_CONNECTION_DRIVER_HELP } from "../utils/driverImportGuidance";
import {
describeUnsupportedOceanBaseProtocol,
normalizeOceanBaseProtocol,
OCEANBASE_PROTOCOL_PARAM_KEYS,
resolveOceanBaseProtocolFromQueryText as resolveOceanBaseProtocolQueryText,
type OceanBaseProtocol,
} from "../utils/oceanBaseProtocol";
import {
applyNoAutoCapAttributes,
noAutoCapInputProps,
@@ -98,7 +105,7 @@ type ChoiceCardOption = {
description?: string;
};
type ClickHouseProtocolChoice = "auto" | "http" | "native";
type OceanBaseProtocolChoice = "mysql" | "oracle";
type OceanBaseProtocolChoice = OceanBaseProtocol;
const MAX_URI_LENGTH = 4096;
const MAX_CONNECTION_PARAMS_LENGTH = 4096;
const MAX_URI_HOSTS = 32;
@@ -122,15 +129,6 @@ const OCEANBASE_PROTOCOL_OPTIONS: Array<{
{ value: "mysql", label: "MySQL" },
{ value: "oracle", label: "Oracle" },
];
const OCEANBASE_PROTOCOL_PARAM_KEYS = [
"protocol",
"oceanBaseProtocol",
"oceanbaseProtocol",
"tenantMode",
"compatMode",
"mode",
];
const normalizeClickHouseProtocolValue = (
value: unknown,
): ClickHouseProtocolChoice => {
@@ -144,10 +142,7 @@ const normalizeClickHouseProtocolValue = (
const normalizeOceanBaseProtocolValue = (
value: unknown,
): OceanBaseProtocolChoice => {
const text = String(value || "")
.trim()
.toLowerCase();
return text === "oracle" ? "oracle" : "mysql";
return normalizeOceanBaseProtocol(value) || "mysql";
};
const resolveOceanBaseProtocolValue = (
value: unknown,
@@ -156,29 +151,12 @@ const resolveOceanBaseProtocolValue = (
.trim()
.toLowerCase();
if (!text) return undefined;
return ["oracle", "oracle-mode", "oracle_mode", "oboracle"].includes(text)
? "oracle"
: "mysql";
return normalizeOceanBaseProtocol(text);
};
const resolveOceanBaseProtocolFromQueryText = (
value: unknown,
): OceanBaseProtocolChoice | undefined => {
let text = String(value || "").trim();
if (!text) return undefined;
const queryIndex = text.indexOf("?");
if (queryIndex >= 0) {
text = text.slice(queryIndex + 1);
}
const hashIndex = text.indexOf("#");
if (hashIndex >= 0) {
text = text.slice(0, hashIndex);
}
const params = new URLSearchParams(text.replace(/^[?&]+/, ""));
for (const key of OCEANBASE_PROTOCOL_PARAM_KEYS) {
const protocol = resolveOceanBaseProtocolValue(params.get(key));
if (protocol) return protocol;
}
return undefined;
return resolveOceanBaseProtocolQueryText(value).protocol;
};
const resolveOceanBaseProtocolForConfig = (
config: Partial<ConnectionConfig>,
@@ -1179,7 +1157,12 @@ const ConnectionModal: React.FC<{
rawParams: unknown,
selectedProtocol: OceanBaseProtocolChoice,
) => {
const params = new URLSearchParams(normalizeConnectionParamsText(rawParams));
const normalizedParamsText = normalizeConnectionParamsText(rawParams);
const protocolFromParams = resolveOceanBaseProtocolQueryText(normalizedParamsText);
if (protocolFromParams.unsupportedValue) {
throw new Error(describeUnsupportedOceanBaseProtocol(protocolFromParams.unsupportedValue));
}
const params = new URLSearchParams(normalizedParamsText);
for (const key of OCEANBASE_PROTOCOL_PARAM_KEYS) {
params.delete(key);
}
@@ -4775,7 +4758,7 @@ const ConnectionModal: React.FC<{
<Form.Item
name="oceanBaseProtocol"
label="OceanBase 协议"
help="MySQL 租户选择 MySQLOracle 租户选择 Oracle。该选择会同时影响连接测试、浏览表结构和 SQL 方言。"
help="MySQL 租户选择 MySQLOracle 租户选择 Oracle。OceanBase 租户兼容模式不包含 Native该选择会同时影响连接测试、浏览表结构和 SQL 方言。"
style={{ marginBottom: 0 }}
>
<Select

View File

@@ -5,6 +5,7 @@ import { TabData } from '../types';
import { useStore } from '../store';
import { DBQuery } from '../../wailsjs/go/app/App';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol';
interface DefinitionViewerProps {
tab: TabData;
@@ -43,11 +44,11 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
if (type === 'custom') {
const driver = String(conn?.config?.driver || '').trim().toLowerCase();
if (driver === 'diros' || driver === 'doris') return 'mysql';
if (driver === 'oceanbase') return 'mysql';
if (driver === 'oceanbase') return normalizeOceanBaseProtocol(conn?.config?.oceanBaseProtocol) === 'oracle' ? 'oracle' : 'mysql';
if (driver === 'opengauss' || driver === 'open_gauss' || driver === 'open-gauss') return 'opengauss';
return driver;
}
if (type === 'oceanbase' && String(conn?.config?.oceanBaseProtocol || '').trim().toLowerCase() === 'oracle') return 'oracle';
if (type === 'oceanbase' && normalizeOceanBaseProtocol(conn?.config?.oceanBaseProtocol) === 'oracle') return 'oracle';
if (type === 'mariadb' || type === 'oceanbase' || type === 'diros' || type === 'sphinx') return 'mysql';
if (type === 'dameng') return 'dm';
return type;

View File

@@ -48,6 +48,7 @@ import FindInDatabaseModal from './FindInDatabaseModal';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { noAutoCapInputProps } from '../utils/inputAutoCap';
import { normalizeSidebarViewName, resolveSidebarRuntimeDatabase } from '../utils/sidebarMetadata';
import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol';
import { resolveConnectionHostTokens } from '../utils/tabDisplay';
import {
findSidebarNodePathByKey,
@@ -686,11 +687,11 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
if (type === 'custom') {
const driver = String(conn?.config?.driver || '').trim().toLowerCase();
if (driver === 'diros' || driver === 'doris') return 'mysql';
if (driver === 'oceanbase') return 'mysql';
if (driver === 'oceanbase') return normalizeOceanBaseProtocol(conn?.config?.oceanBaseProtocol) === 'oracle' ? 'oracle' : 'mysql';
if (driver === 'opengauss' || driver === 'open_gauss' || driver === 'open-gauss') return 'opengauss';
return driver;
}
if (type === 'oceanbase' && String(conn?.config?.oceanBaseProtocol || '').trim().toLowerCase() === 'oracle') return 'oracle';
if (type === 'oceanbase' && normalizeOceanBaseProtocol(conn?.config?.oceanBaseProtocol) === 'oracle') return 'oracle';
if (type === 'mariadb' || type === 'oceanbase' || type === 'diros' || type === 'sphinx') return 'mysql';
if (type === 'dameng') return 'dm';
return type;

View File

@@ -17,6 +17,7 @@ import {
type TableOverviewSortField,
type TableOverviewSortOrder,
} from '../utils/tableOverviewFilter';
import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol';
interface TableOverviewProps {
tab: TabData;
@@ -57,11 +58,11 @@ const getMetadataDialect = (connType: string, driver?: string, oceanBaseProtocol
if (type === 'custom') {
const d = (driver || '').trim().toLowerCase();
if (d === 'diros' || d === 'doris') return 'mysql';
if (d === 'oceanbase') return 'mysql';
if (d === 'oceanbase') return normalizeOceanBaseProtocol(oceanBaseProtocol) === 'oracle' ? 'oracle' : 'mysql';
if (d === 'opengauss' || d === 'open_gauss' || d === 'open-gauss') return 'opengauss';
return d;
}
if (type === 'oceanbase' && String(oceanBaseProtocol || '').trim().toLowerCase() === 'oracle') return 'oracle';
if (type === 'oceanbase' && normalizeOceanBaseProtocol(oceanBaseProtocol) === 'oracle') return 'oracle';
if (type === 'mariadb' || type === 'oceanbase' || type === 'diros' || type === 'sphinx') return 'mysql';
if (type === 'dameng') return 'dm';
return type;

View File

@@ -5,6 +5,7 @@ import { TabData } from '../types';
import { useStore } from '../store';
import { DBQuery } from '../../wailsjs/go/app/App';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol';
interface TriggerViewerProps {
tab: TabData;
@@ -29,11 +30,11 @@ const TriggerViewer: React.FC<TriggerViewerProps> = ({ tab }) => {
if (type === 'custom') {
const driver = String(conn?.config?.driver || '').trim().toLowerCase();
if (driver === 'diros' || driver === 'doris') return 'mysql';
if (driver === 'oceanbase') return 'mysql';
if (driver === 'oceanbase') return normalizeOceanBaseProtocol(conn?.config?.oceanBaseProtocol) === 'oracle' ? 'oracle' : 'mysql';
if (driver === 'opengauss' || driver === 'open_gauss' || driver === 'open-gauss') return 'opengauss';
return driver;
}
if (type === 'oceanbase' && String(conn?.config?.oceanBaseProtocol || '').trim().toLowerCase() === 'oracle') return 'oracle';
if (type === 'oceanbase' && normalizeOceanBaseProtocol(conn?.config?.oceanBaseProtocol) === 'oracle') return 'oracle';
if (type === 'mariadb' || type === 'oceanbase' || type === 'diros' || type === 'sphinx') return 'mysql';
if (type === 'dameng') return 'dm';
return type;

View File

@@ -119,6 +119,36 @@ describe('store appearance persistence', () => {
expect(useStore.getState().connections[0]?.config.password).toBe('secret');
});
it('does not fail hydration when persisted OceanBase connection uses unsupported native protocol', async () => {
storage.setItem('lite-db-storage', JSON.stringify({
state: {
connections: [
{
id: 'oceanbase-native',
name: 'OceanBase Native',
config: {
id: 'oceanbase-native',
type: 'oceanbase',
host: 'ob.local',
port: 2881,
user: 'root@test',
oceanBaseProtocol: 'mysql',
connectionParams: 'protocol=native',
},
},
],
},
version: 9,
}));
const { useStore } = await importStore();
const config = useStore.getState().connections[0]?.config;
expect(useStore.getState().connections).toHaveLength(1);
expect(config?.connectionParams).toBe('protocol=native');
expect(config?.oceanBaseProtocol).toBe('mysql');
});
it('preserves JVM Arthas diagnostic config when replacing saved connections', async () => {
const { useStore } = await importStore();
@@ -294,6 +324,32 @@ describe('store appearance persistence', () => {
);
});
it('keeps saved OceanBase native protocol loadable for connect-time rejection', async () => {
const { useStore } = await importStore();
expect(() => useStore.getState().replaceConnections([
{
id: 'oceanbase-native',
name: 'OceanBase Native',
config: {
id: 'oceanbase-native',
type: 'oceanbase',
host: 'ob.local',
port: 2881,
user: 'root@test',
oceanBaseProtocol: 'mysql',
connectionParams: 'protocol=native',
},
},
])).not.toThrow();
expect(useStore.getState().connections[0]?.config.connectionParams).toBe(
'protocol=native',
);
expect(useStore.getState().connections[0]?.config.oceanBaseProtocol).toBe(
'mysql',
);
});
it('normalizes OceanBase protocol when updating a saved connection', async () => {
const { useStore } = await importStore();

View File

@@ -34,6 +34,11 @@ import {
sanitizeDataGridDisplaySettings,
type DataGridDisplaySettings,
} from "./utils/dataGridDisplay";
import {
normalizeOceanBaseProtocol,
resolveOceanBaseProtocolFromConfig,
resolveOceanBaseProtocolFromQueryText,
} from "./utils/oceanBaseProtocol";
export interface AppearanceSettings extends DataGridDisplaySettings {
enabled: boolean;
@@ -78,68 +83,26 @@ const DEFAULT_GLOBAL_PROXY: GlobalProxyConfig = {
password: "",
hasPassword: false,
};
const OCEANBASE_PROTOCOL_PARAM_KEYS = [
"protocol",
"oceanBaseProtocol",
"oceanbaseProtocol",
"tenantMode",
"compatMode",
"mode",
];
const normalizeOceanBaseProtocol = (
value: unknown,
): "mysql" | "oracle" | undefined => {
const normalized = String(value ?? "").trim().toLowerCase();
if (!normalized) {
return undefined;
}
return normalized === "oracle" ||
normalized === "oracle-mode" ||
normalized === "oracle_mode" ||
normalized === "oboracle"
? "oracle"
: "mysql";
};
const resolveOceanBaseProtocolFromQueryText = (
value: unknown,
): "mysql" | "oracle" | undefined => {
let text = String(value ?? "").trim();
if (!text) {
return undefined;
}
const queryIndex = text.indexOf("?");
if (queryIndex >= 0) {
text = text.slice(queryIndex + 1);
}
const hashIndex = text.indexOf("#");
if (hashIndex >= 0) {
text = text.slice(0, hashIndex);
}
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 = (
raw: Record<string, unknown>,
normalizedConnectionParams: string,
normalizedUri: string,
): "mysql" | "oracle" => {
if (Object.prototype.hasOwnProperty.call(raw, "oceanBaseProtocol")) {
const explicitProtocol = normalizeOceanBaseProtocol(raw.oceanBaseProtocol);
if (explicitProtocol) {
return explicitProtocol;
}
const normalizedConfig = {
...raw,
connectionParams: normalizedConnectionParams,
uri: normalizedUri,
};
try {
return resolveOceanBaseProtocolFromConfig(normalizedConfig);
} catch {
return (
normalizeOceanBaseProtocol(raw.oceanBaseProtocol) ||
resolveOceanBaseProtocolFromQueryText(normalizedConnectionParams).protocol ||
resolveOceanBaseProtocolFromQueryText(normalizedUri).protocol ||
"mysql"
);
}
return (
resolveOceanBaseProtocolFromQueryText(normalizedConnectionParams) ||
resolveOceanBaseProtocolFromQueryText(normalizedUri) ||
"mysql"
);
};
const SUPPORTED_CONNECTION_TYPES = new Set([
"mysql",

View File

@@ -299,7 +299,7 @@ export interface ConnectionConfig {
redisDB?: number; // Redis database index (0-15)
uri?: string; // Connection URI for copy/paste
clickHouseProtocol?: "auto" | "http" | "native"; // ClickHouse connection protocol override
oceanBaseProtocol?: "mysql" | "oracle"; // OceanBase tenant protocol
oceanBaseProtocol?: "mysql" | "oracle"; // OceanBase tenant compatibility protocol
hosts?: string[]; // Multi-host addresses: host:port
topology?: "single" | "replica" | "cluster";
mysqlReplicaUser?: string;

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,

View File

@@ -211,3 +211,50 @@ func TestGetCacheKey_OceanBaseProtocolParamWinsOverAliases(t *testing.T) {
t.Fatalf("expected explicit protocol=mysql to win over alias, got %s vs %s", left, right)
}
}
func TestGetCacheKey_OceanBaseExplicitProtocolOverridesConnectionParams(t *testing.T) {
base := connection.ConnectionConfig{
Type: "oceanbase",
Host: "ob.local",
Port: 2881,
User: "root@test",
Database: "app",
ConnectionParams: "connectTimeout=10",
}
modified := base
modified.OceanBaseProtocol = "mysql"
modified.ConnectionParams = "protocol=oracle&connectTimeout=10"
left := getCacheKey(base)
right := getCacheKey(modified)
if left != right {
t.Fatalf("expected explicit OceanBase protocol=mysql to override params protocol=oracle, got %s vs %s", left, right)
}
}
func TestGetCacheKey_KeepOceanBaseUnsupportedProtocolIsolation(t *testing.T) {
base := connection.ConnectionConfig{
Type: "oceanbase",
Host: "ob.local",
Port: 2881,
User: "root@test",
Database: "app",
ConnectionParams: "protocol=mysql",
}
modified := base
modified.ConnectionParams = "protocol=native"
left := getCacheKey(base)
right := getCacheKey(modified)
if left == right {
t.Fatalf("expected unsupported OceanBase protocol to stay isolated from MySQL cache key")
}
masked := base
masked.OceanBaseProtocol = "mysql"
masked.ConnectionParams = "protocol=native"
if left == getCacheKey(masked) {
t.Fatalf("expected unsupported OceanBase params protocol to stay isolated even with explicit mysql")
}
}

View File

@@ -8,29 +8,53 @@ import (
)
func normalizeOceanBaseProtocolForApp(raw string) string {
switch strings.ToLower(strings.TrimSpace(raw)) {
normalized := strings.ToLower(strings.TrimSpace(raw))
switch normalized {
case "oracle", "oracle-mode", "oracle_mode", "oboracle":
return "oracle"
case "mysql", "mysql-compatible", "mysql_compatible", "mysql-mode", "mysql_mode":
case "mysql", "mysql-compatible", "mysql_compatible", "mysql-mode", "mysql_mode", "obmysql":
return "mysql"
default:
return "mysql"
return normalized
}
}
func isSupportedOceanBaseProtocolForApp(protocol string) bool {
return protocol == "mysql" || protocol == "oracle"
}
func resolveOceanBaseProtocolForApp(config connection.ConnectionConfig) string {
if !strings.EqualFold(strings.TrimSpace(config.Type), "oceanbase") {
return ""
}
explicitProtocol := ""
if explicit := strings.TrimSpace(config.OceanBaseProtocol); explicit != "" {
return normalizeOceanBaseProtocolForApp(explicit)
explicitProtocol = normalizeOceanBaseProtocolForApp(explicit)
if !isSupportedOceanBaseProtocolForApp(explicitProtocol) {
return explicitProtocol
}
}
if protocol := resolveOceanBaseProtocolParam(config.ConnectionParams); protocol != "" {
if !isSupportedOceanBaseProtocolForApp(protocol) {
return protocol
}
if explicitProtocol != "" {
return explicitProtocol
}
return protocol
}
if protocol := resolveOceanBaseProtocolParam(config.URI); protocol != "" {
if !isSupportedOceanBaseProtocolForApp(protocol) {
return protocol
}
if explicitProtocol != "" {
return explicitProtocol
}
return protocol
}
if explicitProtocol != "" {
return explicitProtocol
}
return "mysql"
}
@@ -57,7 +81,7 @@ func resolveOceanBaseProtocolParam(raw string) string {
return ""
}
func normalizeOceanBaseConnectionParamsForCache(raw string) string {
func stripOceanBaseConnectionParamsForCache(raw string) string {
text := strings.TrimSpace(raw)
if text == "" {
return ""
@@ -69,26 +93,40 @@ func normalizeOceanBaseConnectionParamsForCache(raw string) string {
if len(values) == 0 {
return ""
}
protocol := resolveOceanBaseProtocolParam(raw)
for _, key := range []string{"protocol", "oceanBaseProtocol", "oceanbaseProtocol", "tenantMode", "compatMode", "mode"} {
values.Del(key)
}
if strings.EqualFold(protocol, "oracle") {
values.Set("protocol", "oracle")
}
return values.Encode()
}
func normalizeOceanBaseConnectionParamsForCache(raw string) string {
normalized := stripOceanBaseConnectionParamsForCache(raw)
protocol := resolveOceanBaseProtocolParam(raw)
if protocol != "" && !strings.EqualFold(protocol, "mysql") {
values, err := url.ParseQuery(strings.TrimLeft(strings.TrimSpace(normalized), "?&"))
if err != nil {
values = url.Values{}
}
values.Set("protocol", protocol)
return values.Encode()
}
return normalized
}
func normalizeOceanBaseConnectionParamsForCacheWithProtocol(raw string, protocol string) string {
normalized := normalizeOceanBaseConnectionParamsForCache(raw)
if !strings.EqualFold(protocol, "oracle") {
resolvedProtocol := normalizeOceanBaseProtocolForApp(protocol)
if resolvedProtocol == "" {
return normalizeOceanBaseConnectionParamsForCache(raw)
}
normalized := stripOceanBaseConnectionParamsForCache(raw)
if strings.EqualFold(resolvedProtocol, "mysql") {
return normalized
}
values, err := url.ParseQuery(strings.TrimLeft(strings.TrimSpace(normalized), "?&"))
if err != nil {
values = url.Values{}
}
values.Set("protocol", "oracle")
values.Set("protocol", resolvedProtocol)
return values.Encode()
}

View File

@@ -104,7 +104,7 @@ type ConnectionConfig struct {
RedisDB int `json:"redisDB,omitempty"` // Redis database index (0-15)
URI string `json:"uri,omitempty"` // Connection URI for copy/paste
ClickHouseProtocol string `json:"clickHouseProtocol,omitempty"` // auto | http | native
OceanBaseProtocol string `json:"oceanBaseProtocol,omitempty"` // mysql | oracle
OceanBaseProtocol string `json:"oceanBaseProtocol,omitempty"` // OceanBase tenant compatibility protocol: mysql | oracle
Hosts []string `json:"hosts,omitempty"` // Multi-host addresses: host:port
Topology string `json:"topology,omitempty"` // single | replica | cluster
MySQLReplicaUser string `json:"mysqlReplicaUser,omitempty"` // MySQL replica auth user

View File

@@ -151,36 +151,62 @@ func normalizeOceanBaseProtocol(raw string) string {
switch strings.ToLower(strings.TrimSpace(raw)) {
case oceanBaseProtocolOracle, "oracle-mode", "oracle_mode", "oboracle":
return oceanBaseProtocolOracle
case oceanBaseProtocolMySQL, "mysql-compatible", "mysql_compatible", "mysql-mode", "mysql_mode", "":
case oceanBaseProtocolMySQL, "mysql-compatible", "mysql_compatible", "mysql-mode", "mysql_mode", "obmysql", "":
return oceanBaseProtocolMySQL
default:
return oceanBaseProtocolMySQL
return ""
}
}
func resolveOceanBaseProtocolFromValues(values url.Values) string {
func unsupportedOceanBaseProtocolError(raw string) error {
return fmt.Errorf("OceanBase 当前仅支持 MySQL/Oracle 租户协议,不支持 %q请改为 MySQL 或 Oracle", strings.TrimSpace(raw))
}
func resolveOceanBaseProtocolFromValues(values url.Values) (string, error) {
if len(values) == 0 {
return ""
return "", nil
}
for _, key := range []string{"protocol", "oceanBaseProtocol", "oceanbaseProtocol", "tenantMode", "compatMode", "mode"} {
if value := strings.TrimSpace(values.Get(key)); value != "" {
return normalizeOceanBaseProtocol(value)
protocol := normalizeOceanBaseProtocol(value)
if protocol == "" {
return "", unsupportedOceanBaseProtocolError(value)
}
return protocol, nil
}
}
return ""
return "", nil
}
func resolveOceanBaseProtocol(config connection.ConnectionConfig) string {
func resolveOceanBaseProtocol(config connection.ConnectionConfig) (string, error) {
explicitProtocol := ""
if explicit := strings.TrimSpace(config.OceanBaseProtocol); explicit != "" {
return normalizeOceanBaseProtocol(explicit)
protocol := normalizeOceanBaseProtocol(explicit)
if protocol == "" {
return "", unsupportedOceanBaseProtocolError(explicit)
}
explicitProtocol = protocol
}
if protocol := resolveOceanBaseProtocolFromValues(connectionParamsFromText(config.ConnectionParams)); protocol != "" {
return protocol
if protocol, err := resolveOceanBaseProtocolFromValues(connectionParamsFromText(config.ConnectionParams)); err != nil {
return "", err
} else if protocol != "" {
if explicitProtocol != "" {
return explicitProtocol, nil
}
return protocol, nil
}
if protocol := resolveOceanBaseProtocolFromValues(connectionParamsFromURI(config.URI, "oceanbase", "mysql")); protocol != "" {
return protocol
if protocol, err := resolveOceanBaseProtocolFromValues(connectionParamsFromURI(config.URI, "oceanbase", "mysql")); err != nil {
return "", err
} else if protocol != "" {
if explicitProtocol != "" {
return explicitProtocol, nil
}
return protocol, nil
}
return oceanBaseProtocolMySQL
if explicitProtocol != "" {
return explicitProtocol, nil
}
return oceanBaseProtocolMySQL, nil
}
func stripOceanBaseProtocolParams(raw string) string {
@@ -256,7 +282,10 @@ func (o *OceanBaseDB) Connect(config connection.ConnectionConfig) error {
o.oracle = nil
o.protocol = oceanBaseProtocolMySQL
appliedConfig := applyOceanBaseURI(config)
protocol := resolveOceanBaseProtocol(appliedConfig)
protocol, err := resolveOceanBaseProtocol(appliedConfig)
if err != nil {
return err
}
runConfig := withoutOceanBaseProtocolParams(appliedConfig)
if protocol == oceanBaseProtocolOracle {
logger.Infof("OceanBase 使用 Oracle 协议连接:地址=%s:%d 用户=%s", runConfig.Host, runConfig.Port, runConfig.User)

View File

@@ -79,13 +79,52 @@ func TestResolveOceanBaseProtocol(t *testing.T) {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := resolveOceanBaseProtocol(tt.config); got != tt.want {
got, err := resolveOceanBaseProtocol(tt.config)
if err != nil {
t.Fatalf("resolveOceanBaseProtocol() unexpected error: %v", err)
}
if got != tt.want {
t.Fatalf("resolveOceanBaseProtocol() = %q, want %q", got, tt.want)
}
})
}
}
func TestResolveOceanBaseProtocolRejectsUnsupportedNative(t *testing.T) {
t.Parallel()
tests := []struct {
name string
config connection.ConnectionConfig
}{
{
name: "params native",
config: connection.ConnectionConfig{
Type: "oceanbase",
ConnectionParams: "protocol=native",
},
},
{
name: "explicit mysql does not mask params native",
config: connection.ConnectionConfig{
Type: "oceanbase",
OceanBaseProtocol: "mysql",
ConnectionParams: "protocol=native",
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
_, err := resolveOceanBaseProtocol(tt.config)
if err == nil || !strings.Contains(err.Error(), "不支持") {
t.Fatalf("expected unsupported protocol error, got %v", err)
}
})
}
}
func TestWithoutOceanBaseProtocolParamsStripsDriverMeta(t *testing.T) {
t.Parallel()