+ {denseLabel(
+ t("connection.modal.dense.mode"),
+ t("connection.modal.field.oracleMode.label"),
+ )}
+
+
+
+
+ {t("connection.modal.field.oracleMode.service")}
+
+
+ {t("connection.modal.field.oracleMode.sid")}
+
+
+
+
+
+ )}
+
{(dbType === "oracle" || isOceanBaseOracle) && (
{denseLabel(
t("connection.modal.dense.service"),
- isOceanBaseOracle
- ? t("connection.modal.field.oceanBaseServiceName.label")
- : t("connection.modal.field.serviceName.label"),
+ dbType === "oracle" && oracleMode === "sid"
+ ? t("connection.modal.field.sid.label")
+ : isOceanBaseOracle
+ ? t("connection.modal.field.oceanBaseServiceName.label")
+ : t("connection.modal.field.serviceName.label"),
)}
= (props) => {
? []
: [
createUriAwareRequiredRule(
- t("connection.modal.field.serviceName.required"),
+ dbType === "oracle" && oracleMode === "sid"
+ ? t("connection.modal.field.sid.required")
+ : t(
+ "connection.modal.field.serviceName.required",
+ ),
),
]
}
@@ -1396,9 +1426,13 @@ const ConnectionModalStep2: React.FC = (props) => {
>
@@ -2639,6 +2673,7 @@ const ConnectionModalStep2: React.FC
= (props) => {
restrictScriptExecution: false,
restrictDataImport: false,
oceanBaseProtocol: "mysql",
+ oracleMode: "service",
mysqlTopology: "single",
rocketmqTopology: "single",
mqttTopology: "single",
diff --git a/frontend/src/components/connectionModal/connectionModalConfig.oracle.test.ts b/frontend/src/components/connectionModal/connectionModalConfig.oracle.test.ts
new file mode 100644
index 00000000..344516f3
--- /dev/null
+++ b/frontend/src/components/connectionModal/connectionModalConfig.oracle.test.ts
@@ -0,0 +1,256 @@
+import { describe, expect, it } from "vitest";
+
+import { buildConnectionConfig } from "./connectionModalConfig";
+
+const translate = (key: string) => key;
+
+const buildOracleValues = (overrides: Record = {}) => ({
+ type: "oracle",
+ host: "db.example.test",
+ port: 1521,
+ user: "system",
+ password: "secret",
+ database: "ORCLPDB1",
+ oracleMode: "service",
+ useSSL: false,
+ useSSH: false,
+ useProxy: false,
+ useHttpTunnel: false,
+ timeout: 30,
+ keepAliveEnabled: false,
+ keepAliveIntervalMinutes: 240,
+ keepAliveSQL: "",
+ savePassword: true,
+ uri: "",
+ connectionParams: "",
+ sslMode: "preferred",
+ sslCAPath: "",
+ sslCertPath: "",
+ sslKeyPath: "",
+ sshHost: "",
+ sshPort: 22,
+ sshUser: "",
+ sshPassword: "",
+ sshKeyPath: "",
+ proxyType: "socks5",
+ proxyHost: "",
+ proxyPort: 1080,
+ proxyUser: "",
+ proxyPassword: "",
+ httpTunnelHost: "",
+ httpTunnelPort: 8080,
+ httpTunnelUser: "",
+ httpTunnelPassword: "",
+ restrictDataEdit: false,
+ restrictStructureEdit: false,
+ restrictScriptExecution: false,
+ restrictDataImport: false,
+ ...overrides,
+});
+
+describe("connectionModalConfig Oracle SID mode", () => {
+ it("keeps service-name mode untouched (SID removed from params)", async () => {
+ const config = await buildConnectionConfig({
+ values: buildOracleValues({
+ connectionParams: "DBA_PRIVILEGE=SYSDBA&SID=ORCL",
+ }),
+ forPersist: true,
+ oracleModeTouched: true,
+ translate,
+ });
+
+ expect(config.database).toBe("ORCLPDB1");
+ const params = new URLSearchParams(config.connectionParams);
+ expect(params.get("DBA_PRIVILEGE")).toBe("SYSDBA");
+ expect(params.has("SID")).toBe(false);
+ });
+
+ it("writes the form SID value into connectionParams and clears database", async () => {
+ const config = await buildConnectionConfig({
+ values: buildOracleValues({
+ oracleMode: "sid",
+ database: "ORCL",
+ connectionParams: "DBA_PRIVILEGE=SYSDBA",
+ }),
+ forPersist: true,
+ translate,
+ });
+
+ expect(config.database).toBe("");
+ const params = new URLSearchParams(config.connectionParams);
+ expect(params.get("SID")).toBe("ORCL");
+ expect(params.get("DBA_PRIVILEGE")).toBe("SYSDBA");
+ });
+
+ it("replaces a legacy SID param when switching to SID mode", async () => {
+ const config = await buildConnectionConfig({
+ values: buildOracleValues({
+ oracleMode: "sid",
+ database: "ORCL",
+ connectionParams: "sid=OLDDB",
+ }),
+ forPersist: true,
+ translate,
+ });
+
+ const params = new URLSearchParams(config.connectionParams);
+ expect(params.get("SID")).toBe("ORCL");
+ expect(config.database).toBe("");
+ });
+
+ it("clears a SID param when switching back to service-name mode", async () => {
+ const config = await buildConnectionConfig({
+ values: buildOracleValues({
+ oracleMode: "service",
+ database: "ORCLPDB1",
+ connectionParams: "SID=ORCL",
+ }),
+ forPersist: true,
+ oracleModeTouched: true,
+ translate,
+ });
+
+ expect(config.database).toBe("ORCLPDB1");
+ expect(new URLSearchParams(config.connectionParams).has("SID")).toBe(false);
+ });
+
+ it("strips a SID param from the URI when switching back to service-name mode", async () => {
+ const config = await buildConnectionConfig({
+ values: buildOracleValues({
+ oracleMode: "service",
+ database: "ORCLPDB1",
+ uri: "oracle://system:secret@db.example.test:1521/?SID=ORCL&DBA_PRIVILEGE=SYSDBA",
+ }),
+ forPersist: true,
+ oracleModeTouched: true,
+ translate,
+ });
+
+ expect(config.database).toBe("ORCLPDB1");
+ expect(config.uri).toBe(
+ "oracle://system:secret@db.example.test:1521/?DBA_PRIVILEGE=SYSDBA",
+ );
+ });
+
+ it("rejects an empty SID value", async () => {
+ await expect(
+ buildConnectionConfig({
+ values: buildOracleValues({ oracleMode: "sid", database: "" }),
+ forPersist: true,
+ translate,
+ }),
+ ).rejects.toThrow("connection.modal.field.sid.required");
+ });
+
+ it("infers SID mode when a SID URI is saved without parsing first", async () => {
+ const config = await buildConnectionConfig({
+ values: buildOracleValues({
+ database: "STALE_SERVICE",
+ connectionParams: "DBA_PRIVILEGE=SYSDBA",
+ uri: "oracle://system:secret@db.example.test:1521?SID=ORCL",
+ }),
+ forPersist: true,
+ translate,
+ });
+
+ expect(config.database).toBe("");
+ expect(new URLSearchParams(config.connectionParams).get("SID")).toBe("ORCL");
+ expect(new URLSearchParams(config.connectionParams).get("DBA_PRIVILEGE")).toBe(
+ "SYSDBA",
+ );
+ });
+
+ it("keeps an explicitly selected service name when the URI contains SID", async () => {
+ const config = await buildConnectionConfig({
+ values: buildOracleValues({
+ database: "ORCLPDB1",
+ uri: "oracle://system:secret@db.example.test:1521?SID=ORCL",
+ }),
+ forPersist: true,
+ oracleModeTouched: true,
+ translate,
+ });
+
+ expect(config.database).toBe("ORCLPDB1");
+ expect(config.uri).toBe(
+ "oracle://system:secret@db.example.test:1521",
+ );
+ expect(new URLSearchParams(config.connectionParams).has("SID")).toBe(false);
+ });
+
+ it("requires a service name when an explicit service choice conflicts with a SID URI", async () => {
+ await expect(
+ buildConnectionConfig({
+ values: buildOracleValues({
+ database: "",
+ uri: "oracle://system:secret@db.example.test:1521?SID=ORCL",
+ }),
+ forPersist: true,
+ oracleModeTouched: true,
+ translate,
+ }),
+ ).rejects.toThrow("connection.modal.field.serviceName.required");
+ });
+
+ it("lets an explicit empty SID param override a SID from the URI", async () => {
+ const config = await buildConnectionConfig({
+ values: buildOracleValues({
+ database: "ORCLPDB1",
+ connectionParams: "SID=&DBA_PRIVILEGE=SYSDBA",
+ uri: "oracle://system:secret@db.example.test:1521?SID=FROM_URI",
+ }),
+ forPersist: true,
+ translate,
+ });
+
+ expect(config.database).toBe("ORCLPDB1");
+ expect(config.uri).toBe(
+ "oracle://system:secret@db.example.test:1521",
+ );
+ expect(new URLSearchParams(config.connectionParams).has("SID")).toBe(false);
+ });
+
+ it("does not reinterpret an overridden URI SID as a service name", async () => {
+ await expect(
+ buildConnectionConfig({
+ values: buildOracleValues({
+ database: "",
+ connectionParams: "SID=",
+ uri: "oracle://system:secret@db.example.test:1521?SID=FROM_URI",
+ }),
+ forPersist: true,
+ translate,
+ }),
+ ).rejects.toThrow("connection.modal.field.serviceName.required");
+ });
+
+ it("does not reinterpret a service-name URI path as SID", async () => {
+ await expect(
+ buildConnectionConfig({
+ values: buildOracleValues({
+ oracleMode: "sid",
+ database: "",
+ uri: "oracle://system:secret@db.example.test:1521/ORCLPDB1",
+ }),
+ forPersist: true,
+ translate,
+ }),
+ ).rejects.toThrow("connection.modal.field.sid.required");
+ });
+
+ it("does not replace an edited SID value with the stored connection param", async () => {
+ const config = await buildConnectionConfig({
+ values: buildOracleValues({
+ oracleMode: "sid",
+ database: "NEW_SID",
+ connectionParams: "SID=OLD_SID",
+ }),
+ forPersist: true,
+ translate,
+ });
+
+ expect(new URLSearchParams(config.connectionParams).get("SID")).toBe(
+ "NEW_SID",
+ );
+ });
+});
diff --git a/frontend/src/components/connectionModal/connectionModalConfig.ts b/frontend/src/components/connectionModal/connectionModalConfig.ts
index 8ea118e5..adaa853d 100644
--- a/frontend/src/components/connectionModal/connectionModalConfig.ts
+++ b/frontend/src/components/connectionModal/connectionModalConfig.ts
@@ -39,7 +39,11 @@ import {
parseClickHouseHTTPUriToValues,
parseHostPort,
parseUriToValues,
+ resolveOracleConnectionTarget,
toAddress,
+ withOracleSIDParam,
+ withoutOracleSIDFromURI,
+ withoutOracleSIDParam,
} from "./connectionModalUri";
type Translate = (key: string, params?: any) => string;
@@ -116,6 +120,7 @@ type BuildConnectionConfigParams = {
forPersist: boolean;
initialValues?: SavedConnection | null;
nacosNamespaceIdTouched?: boolean;
+ oracleModeTouched?: boolean;
translate: Translate;
};
@@ -350,6 +355,7 @@ export const buildConnectionConfig = async ({
forPersist,
initialValues,
nacosNamespaceIdTouched = false,
+ oracleModeTouched = false,
translate: t,
}: BuildConnectionConfigParams): Promise => {
const mergedValues = { ...values };
@@ -477,6 +483,40 @@ export const buildConnectionConfig = async ({
mergedValues.uri,
mergedValues.type,
);
+ const isOracleConfig =
+ String(mergedValues.type || "")
+ .trim()
+ .toLowerCase() === "oracle";
+ const parsedOracleMode = String(parsedUriValues?.oracleMode || "")
+ .trim()
+ .toLowerCase();
+ const currentOracleMode =
+ String(mergedValues.oracleMode || "service")
+ .trim()
+ .toLowerCase() === "sid"
+ ? "sid"
+ : "service";
+ const oracleTarget = isOracleConfig
+ ? resolveOracleConnectionTarget(
+ parsedUriValues?.connectionParams,
+ mergedValues.connectionParams,
+ )
+ : null;
+ const resolvedOracleMode = isOracleConfig
+ ? oracleModeTouched || currentOracleMode === "sid"
+ ? currentOracleMode
+ : oracleTarget?.mode || "service"
+ : "";
+ if (isOracleConfig && resolvedOracleMode === "sid") {
+ mergedValues.oracleMode = "sid";
+ if (
+ oracleTarget?.mode === "sid" &&
+ (currentOracleMode !== "sid" ||
+ String(mergedValues.database || "").trim() === "")
+ ) {
+ mergedValues.database = oracleTarget.sid;
+ }
+ }
const isEmptyField = (value: unknown) =>
value === undefined ||
value === null ||
@@ -485,6 +525,14 @@ export const buildConnectionConfig = async ({
(Array.isArray(value) && value.length === 0);
if (parsedUriValues) {
Object.entries(parsedUriValues).forEach(([key, value]) => {
+ if (
+ isOracleConfig &&
+ parsedOracleMode &&
+ parsedOracleMode !== resolvedOracleMode &&
+ (key === "database" || key === "oracleMode")
+ ) {
+ return;
+ }
if (key === "nacosNamespaceId" && hasExplicitNacosNamespaceId) {
return;
}
@@ -874,6 +922,38 @@ export const buildConnectionConfig = async ({
? `${normalizedConnectionParams}&contextPath=/nacos`
: "contextPath=/nacos";
}
+ if (type === "oracle") {
+ const oracleMode =
+ String(mergedValues.oracleMode || "service")
+ .trim()
+ .toLowerCase() === "sid"
+ ? "sid"
+ : "service";
+ const oracleTargetValue = String(mergedValues.database || "").trim();
+ if (!oracleTargetValue) {
+ throw new Error(
+ t(
+ oracleMode === "sid"
+ ? "connection.modal.field.sid.required"
+ : "connection.modal.field.serviceName.required",
+ ),
+ );
+ }
+ if (oracleMode === "sid") {
+ // SID 模式:表单 database 字段承载 SID 值,写入 connectionParams 的 SID 参数,
+ // Database(服务名)置空避免 DSN path 冗余(后端 getDSN 据此组装 (SID=...))。
+ normalizedConnectionParams = withOracleSIDParam(
+ normalizedConnectionParams,
+ oracleTargetValue,
+ );
+ mergedValues.database = "";
+ } else {
+ // 服务名模式:清除历史 SID 参数(含 URI query),避免 go-ora 驱动 SID 优先导致连接目标漂移。
+ normalizedConnectionParams =
+ withoutOracleSIDParam(normalizedConnectionParams);
+ mergedValues.uri = withoutOracleSIDFromURI(mergedValues.uri);
+ }
+ }
const supportsProductionGuard = supportsConnectionReadOnlyMode({
type,
driver: mergedValues.driver,
diff --git a/frontend/src/components/connectionModal/connectionModalUri.test.ts b/frontend/src/components/connectionModal/connectionModalUri.test.ts
index 364338aa..0531ee8e 100644
--- a/frontend/src/components/connectionModal/connectionModalUri.test.ts
+++ b/frontend/src/components/connectionModal/connectionModalUri.test.ts
@@ -2,10 +2,15 @@ import { describe, expect, it } from 'vitest';
import {
buildUriFromValues,
+ extractOracleSIDParam,
getConnectionParamsPlaceholder,
getUriPlaceholder,
parseTrinoUriToValues,
parseUriToValues,
+ resolveOracleConnectionTarget,
+ withOracleSIDParam,
+ withoutOracleSIDFromURI,
+ withoutOracleSIDParam,
} from './connectionModalUri';
describe('connectionModalUri trino support', () => {
@@ -160,3 +165,165 @@ describe('connectionModalUri Nacos support', () => {
);
});
});
+
+describe('connectionModalUri Oracle SID support', () => {
+ it('resolves SID using the same URI then connection-params precedence as the backend', () => {
+ expect(resolveOracleConnectionTarget('SID=FROM_URI')).toEqual({
+ mode: 'sid',
+ sid: 'FROM_URI',
+ });
+ expect(
+ resolveOracleConnectionTarget(
+ 'SID=FROM_URI',
+ 'DBA_PRIVILEGE=SYSDBA',
+ ),
+ ).toEqual({ mode: 'sid', sid: 'FROM_URI' });
+ expect(
+ resolveOracleConnectionTarget('SID=FROM_URI', 'sid=FROM_PARAMS'),
+ ).toEqual({ mode: 'sid', sid: 'FROM_PARAMS' });
+ expect(resolveOracleConnectionTarget('SID=FROM_URI', 'SID=')).toEqual({
+ mode: 'service',
+ sid: '',
+ });
+ });
+
+ it('parses SID-only Oracle URIs and lets SID override a legacy path', () => {
+ expect(
+ parseUriToValues(
+ 'oracle://system:secret@db.example.test:1521?SID=ORCL',
+ 'oracle',
+ ),
+ ).toMatchObject({
+ host: 'db.example.test',
+ port: 1521,
+ database: 'ORCL',
+ oracleMode: 'sid',
+ connectionParams: 'SID=ORCL',
+ });
+ expect(
+ parseUriToValues(
+ 'oracle://system:secret@db.example.test:1521/OLD_SERVICE?SID=ORCL',
+ 'oracle',
+ ),
+ ).toMatchObject({ database: 'ORCL', oracleMode: 'sid' });
+ expect(
+ parseUriToValues(
+ 'oracle://system:secret@db.example.test:1521/ORCLPDB1',
+ 'oracle',
+ ),
+ ).toMatchObject({ database: 'ORCLPDB1', oracleMode: 'service' });
+ expect(
+ parseUriToValues('oracle://system:secret@db.example.test:1521', 'oracle'),
+ ).toBeNull();
+ });
+
+ it('generates Oracle URIs according to the selected connection mode', () => {
+ const sidUri = new URL(
+ buildUriFromValues({
+ type: 'oracle',
+ host: 'db.example.test',
+ port: 1521,
+ user: 'system',
+ password: 'secret',
+ database: 'ORCL',
+ oracleMode: 'sid',
+ connectionParams: 'DBA_PRIVILEGE=SYSDBA&sid=OLD',
+ useSSL: false,
+ }),
+ );
+ expect(sidUri.pathname).toBe('');
+ expect(sidUri.searchParams.get('SID')).toBe('ORCL');
+ expect(sidUri.searchParams.get('DBA_PRIVILEGE')).toBe('SYSDBA');
+
+ const serviceUri = new URL(
+ buildUriFromValues({
+ type: 'oracle',
+ host: 'db.example.test',
+ port: 1521,
+ user: 'system',
+ password: 'secret',
+ database: 'ORCLPDB1',
+ oracleMode: 'service',
+ connectionParams: 'SID=ORCL&DBA_PRIVILEGE=SYSDBA',
+ useSSL: false,
+ }),
+ );
+ expect(serviceUri.pathname).toBe('/ORCLPDB1');
+ expect(serviceUri.searchParams.has('SID')).toBe(false);
+ expect(serviceUri.searchParams.get('DBA_PRIVILEGE')).toBe('SYSDBA');
+ });
+
+ it('keeps SID mode through an Oracle URI parse and generate round trip', () => {
+ const parsed = parseUriToValues(
+ 'oracle://system:secret@db.example.test:1521?SID=ORCL&DBA_PRIVILEGE=SYSDBA',
+ 'oracle',
+ );
+ expect(parsed).not.toBeNull();
+
+ const rebuilt = buildUriFromValues({ type: 'oracle', ...parsed });
+ const reparsed = parseUriToValues(rebuilt, 'oracle');
+ expect(reparsed).toMatchObject({
+ database: 'ORCL',
+ oracleMode: 'sid',
+ });
+ expect(new URL(rebuilt).pathname).toBe('');
+ });
+
+ it('extracts SID case-insensitively from connection params text', () => {
+ expect(extractOracleSIDParam('')).toBe('');
+ expect(extractOracleSIDParam('DBA_PRIVILEGE=SYSDBA')).toBe('');
+ expect(extractOracleSIDParam('SID=ORCL')).toBe('ORCL');
+ expect(extractOracleSIDParam('sid=orcl&SERVICE_NAME=svc')).toBe('orcl');
+ expect(extractOracleSIDParam('PREFETCH_ROWS=50&SID=ORCLPDB')).toBe('ORCLPDB');
+ expect(extractOracleSIDParam('?SID=ORCL')).toBe('ORCL');
+ expect(extractOracleSIDParam(undefined)).toBe('');
+ });
+
+ it('withOracleSIDParam sets a new SID while preserving other params', () => {
+ expect(withOracleSIDParam('', 'ORCL')).toBe('SID=ORCL');
+ expect(withOracleSIDParam('DBA_PRIVILEGE=SYSDBA', 'ORCL')).toBe(
+ 'DBA_PRIVILEGE=SYSDBA&SID=ORCL',
+ );
+ expect(withOracleSIDParam('SID=OLD', 'ORCL')).toBe('SID=ORCL');
+ expect(withOracleSIDParam('SID=OLD&sid=OTHER', 'ORCL')).toBe('SID=ORCL');
+ expect(withOracleSIDParam('sid=old&TRACE FILE=/tmp/x', 'ORCL')).toBe(
+ 'SID=ORCL&TRACE+FILE=%2Ftmp%2Fx',
+ );
+ expect(withOracleSIDParam('SID=OLD', '')).toBe('');
+ });
+
+ it('withoutOracleSIDParam removes SID while preserving other params', () => {
+ expect(withoutOracleSIDParam('')).toBe('');
+ expect(withoutOracleSIDParam('SID=ORCL')).toBe('');
+ expect(withoutOracleSIDParam('sid=orcl&DBA_PRIVILEGE=SYSDBA')).toBe(
+ 'DBA_PRIVILEGE=SYSDBA',
+ );
+ expect(withoutOracleSIDParam('DBA_PRIVILEGE=SYSDBA')).toBe(
+ 'DBA_PRIVILEGE=SYSDBA',
+ );
+ });
+
+ it('withoutOracleSIDFromURI strips SID from a connection URI query', () => {
+ expect(withoutOracleSIDFromURI('')).toBe('');
+ expect(withoutOracleSIDFromURI('oracle://u:p@h:1521/ORCL')).toBe(
+ 'oracle://u:p@h:1521/ORCL',
+ );
+ expect(withoutOracleSIDFromURI('oracle://u:p@h:1521/?SID=ORCL')).toBe(
+ 'oracle://u:p@h:1521/',
+ );
+ expect(
+ withoutOracleSIDFromURI('oracle://u:p@h:1521/?sid=orcl&DBA_PRIVILEGE=SYSDBA'),
+ ).toBe('oracle://u:p@h:1521/?DBA_PRIVILEGE=SYSDBA');
+ expect(
+ withoutOracleSIDFromURI('oracle://u:p@h:1521/?DBA_PRIVILEGE=SYSDBA&SID=ORCL&TRACE=1'),
+ ).toBe('oracle://u:p@h:1521/?DBA_PRIVILEGE=SYSDBA&TRACE=1');
+ expect(
+ withoutOracleSIDFromURI('oracle://u:p@h:1521/?SID=ORCL#frag'),
+ ).toBe('oracle://u:p@h:1521/#frag');
+ expect(
+ withoutOracleSIDFromURI(
+ 'oracle://u:p@h:1521/ORCLPDB1?S%49D=ORCL&TRACE=1',
+ ),
+ ).toBe('oracle://u:p@h:1521/ORCLPDB1?TRACE=1');
+ });
+});
diff --git a/frontend/src/components/connectionModal/connectionModalUri.ts b/frontend/src/components/connectionModal/connectionModalUri.ts
index da6f04b8..fce2f7ec 100644
--- a/frontend/src/components/connectionModal/connectionModalUri.ts
+++ b/frontend/src/components/connectionModal/connectionModalUri.ts
@@ -197,6 +197,118 @@ const serializeConnectionParams = (params: URLSearchParams) => {
return cloned.toString().slice(0, MAX_CONNECTION_PARAMS_LENGTH);
};
+// Oracle 连接定位模式:SID 查询参数。后端白名单与 go-ora 驱动均对参数名做
+// 大小写归一化(oracleConnectionParamNames / ParseConfig 中 strings.ToUpper),
+// 因此这里按大小写不敏感方式读写,避免表单与历史数据大小写不一致导致重复参数。
+
+type OracleConnectionMode = "service" | "sid";
+
+type OracleSIDParamState = {
+ present: boolean;
+ value: string;
+};
+
+const isOracleSIDKey = (key: unknown): boolean =>
+ String(key || "").trim().toUpperCase() === "SID";
+
+const readOracleSIDParam = (rawParams: unknown): OracleSIDParamState => {
+ const text = normalizeConnectionParamsText(rawParams);
+ const params = new URLSearchParams(text);
+ const state: OracleSIDParamState = { present: false, value: "" };
+ params.forEach((value, key) => {
+ if (isOracleSIDKey(key)) {
+ state.present = true;
+ state.value = String(value || "").trim();
+ }
+ });
+ return state;
+};
+
+export const resolveOracleConnectionTarget = (
+ uriParams: unknown,
+ connectionParams?: unknown,
+): { mode: OracleConnectionMode; sid: string } => {
+ const uriSID = readOracleSIDParam(uriParams);
+ const connectionSID = readOracleSIDParam(connectionParams);
+ // 与后端 mergeConnectionParamsFromConfigWithAllowlist 保持一致:
+ // URI 参数先加载,ConnectionParams 中显式存在的 SID(包括空值)后覆盖。
+ const resolvedSID = connectionSID.present ? connectionSID : uriSID;
+ return {
+ mode: resolvedSID.value ? "sid" : "service",
+ sid: resolvedSID.value,
+ };
+};
+
+export const extractOracleSIDParam = (rawParams: unknown): string => {
+ return readOracleSIDParam(rawParams).value;
+};
+
+export const withOracleSIDParam = (
+ rawParams: unknown,
+ sidValue: string,
+): string => {
+ const text = normalizeConnectionParamsText(rawParams);
+ const params = new URLSearchParams(text);
+ const sid = String(sidValue || "").trim();
+ const hasExistingSID = readOracleSIDParam(text).present;
+ if (!hasExistingSID && !sid) return text;
+ const rebuilt = new URLSearchParams();
+ let sidWritten = false;
+ params.forEach((value, key) => {
+ if (isOracleSIDKey(key)) {
+ if (sid && !sidWritten) {
+ rebuilt.append("SID", sid);
+ sidWritten = true;
+ }
+ } else {
+ rebuilt.append(key, value);
+ }
+ });
+ if (!sidWritten && sid) rebuilt.append("SID", sid);
+ return serializeConnectionParams(rebuilt);
+};
+
+export const withoutOracleSIDParam = (rawParams: unknown): string => {
+ const text = normalizeConnectionParamsText(rawParams);
+ if (!text) return "";
+ const params = new URLSearchParams(text);
+ const rebuilt = new URLSearchParams();
+ params.forEach((value, key) => {
+ if (!isOracleSIDKey(key)) {
+ rebuilt.append(key, value);
+ }
+ });
+ return serializeConnectionParams(rebuilt);
+};
+
+// 服务名模式下从连接 URI 中剥离 SID 查询参数(大小写不敏感)。
+// 直接基于 query 分段处理而不是重建 URL 对象,避免对用户粘贴的 URI
+// 引入协议规范化/重新编码等意外差异。
+export const withoutOracleSIDFromURI = (uriText: unknown): string => {
+ const text = String(uriText || "").trim();
+ if (!text) return text;
+ const hashIndex = text.indexOf("#");
+ const beforeHash = hashIndex >= 0 ? text.slice(0, hashIndex) : text;
+ const hash = hashIndex >= 0 ? text.slice(hashIndex) : "";
+ const queryIndex = beforeHash.indexOf("?");
+ if (queryIndex < 0) return text;
+ const base = beforeHash.slice(0, queryIndex);
+ const query = beforeHash.slice(queryIndex + 1);
+ const kept = query.split("&").filter((pair) => {
+ const eq = pair.indexOf("=");
+ const rawKey = (eq >= 0 ? pair.slice(0, eq) : pair).trim();
+ let decodedKey = rawKey;
+ try {
+ decodedKey = decodeURIComponent(rawKey.replace(/\+/g, " "));
+ } catch {
+ // 保留格式异常的原始参数;后端同样不会把无法解析的 query 识别为 SID。
+ }
+ return !isOracleSIDKey(decodedKey);
+ });
+ const rebuilt = kept.length > 0 ? `${base}?${kept.join("&")}` : base;
+ return rebuilt + hash;
+};
+
export const normalizeOceanBaseConnectionParamsText = (
rawParams: unknown,
selectedProtocol: OceanBaseProtocolChoice,
@@ -1009,20 +1121,30 @@ export const parseUriToValues = (
singleHostSchemes,
getDefaultPortByType(type),
);
- if (!parsed) {
- return null;
- }
- if (type === "oracle" && !String(parsed.database || "").trim()) {
- // Oracle 需要显式 service name,避免 URI 解析后放过必填校验。
- return null;
- }
- const parsedValues: Record = {
- host: parsed.host,
- port: parsed.port,
- user: parsed.username,
- password: parsed.password,
- database: parsed.database,
- };
+ if (!parsed) {
+ return null;
+ }
+ const oracleTarget =
+ type === "oracle"
+ ? resolveOracleConnectionTarget(parsed.params.toString())
+ : null;
+ if (
+ type === "oracle" &&
+ !String(parsed.database || "").trim() &&
+ !oracleTarget?.sid
+ ) {
+ // Oracle 必须提供 Service Name path 或 SID 查询参数之一。
+ return null;
+ }
+ const parsedValues: Record = {
+ host: parsed.host,
+ port: parsed.port,
+ user: parsed.username,
+ password: parsed.password,
+ database:
+ oracleTarget?.mode === "sid" ? oracleTarget.sid : parsed.database,
+ ...(oracleTarget ? { oracleMode: oracleTarget.mode } : {}),
+ };
if (supportsConnectionParamsForType(type)) {
parsedValues.connectionParams = serializeConnectionParams(parsed.params);
}
@@ -1624,7 +1746,13 @@ export const buildUriFromValues = (values: any) => {
? "https"
: "http"
: type;
- const dbPath = database ? `/${encodeURIComponent(database)}` : "";
+ const oracleSIDMode =
+ type === "oracle" &&
+ String(values.oracleMode || "service")
+ .trim()
+ .toLowerCase() === "sid";
+ const dbPath =
+ !oracleSIDMode && database ? `/${encodeURIComponent(database)}` : "";
const params = new URLSearchParams();
if (supportsSSLForType(type) && values.useSSL) {
const mode = String(values.sslMode || "preferred")
@@ -1691,6 +1819,11 @@ export const buildUriFromValues = (values: any) => {
if (supportsConnectionParamsForType(type)) {
mergeConnectionParams(params, values.connectionParams);
}
- const query = params.toString();
- return `${scheme}://${encodedAuth}${toAddress(host, port, defaultPort)}${dbPath}${query ? `?${query}` : ""}`;
-};
+ let query = params.toString();
+ if (type === "oracle") {
+ query = oracleSIDMode
+ ? withOracleSIDParam(query, database)
+ : withoutOracleSIDParam(query);
+ }
+ return `${scheme}://${encodedAuth}${toAddress(host, port, defaultPort)}${dbPath}${query ? `?${query}` : ""}`;
+};
diff --git a/frontend/src/i18n/i18n.test.ts b/frontend/src/i18n/i18n.test.ts
index fce08ac5..6a0252d6 100644
--- a/frontend/src/i18n/i18n.test.ts
+++ b/frontend/src/i18n/i18n.test.ts
@@ -292,6 +292,30 @@ const remainingConnectionModalSliceExpectations: LocalizedExpectation[] = [
key: "connection.modal.field.serviceName.placeholder",
catalogKey: "connection_modal.field.serviceName.placeholder",
},
+ {
+ key: "connection.modal.field.oracleMode.label",
+ catalogKey: "connection_modal.field.oracle_mode.label",
+ },
+ {
+ key: "connection.modal.field.oracleMode.service",
+ catalogKey: "connection_modal.field.oracle_mode.service",
+ },
+ {
+ key: "connection.modal.field.oracleMode.sid",
+ catalogKey: "connection_modal.field.oracle_mode.sid",
+ },
+ {
+ key: "connection.modal.field.sid.label",
+ catalogKey: "connection_modal.field.sid.label",
+ },
+ {
+ key: "connection.modal.field.sid.required",
+ catalogKey: "connection_modal.field.sid.required",
+ },
+ {
+ key: "connection.modal.field.sid.placeholder",
+ catalogKey: "connection_modal.field.sid.placeholder",
+ },
];
describe("i18n", () => {
@@ -338,4 +362,22 @@ describe("i18n", () => {
}
},
);
+
+ it("resolves the Oracle mode keys in zh-CN and en-US (legacy messages)", () => {
+ expect(
+ t("connection.modal.field.oracleMode.service", undefined, "zh-CN"),
+ ).toBe("服务名称");
+ expect(t("connection.modal.field.oracleMode.sid", undefined, "zh-CN")).toBe(
+ "SID",
+ );
+ expect(
+ t("connection.modal.field.oracleMode.service", undefined, "en-US"),
+ ).toBe("Service name");
+ expect(t("connection.modal.field.oracleMode.sid", undefined, "en-US")).toBe(
+ "SID",
+ );
+ expect(
+ t("connection.modal.field.sid.required", undefined, "zh-CN"),
+ ).toBe("请输入 SID");
+ });
});
diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts
index 4d2f3c89..3f5631d1 100644
--- a/frontend/src/i18n/index.ts
+++ b/frontend/src/i18n/index.ts
@@ -112,6 +112,24 @@ const catalogAliases: Record = {
"connection_modal.field.serviceName.help": {
aliasKey: "connection_modal.help.oracle_service_name",
},
+ "connection_modal.field.oracleMode.label": {
+ aliasKey: "connection_modal.field.oracle_mode.label",
+ },
+ "connection_modal.field.oracleMode.service": {
+ aliasKey: "connection_modal.field.oracle_mode.service",
+ },
+ "connection_modal.field.oracleMode.sid": {
+ aliasKey: "connection_modal.field.oracle_mode.sid",
+ },
+ "connection_modal.field.sid.label": {
+ aliasKey: "connection_modal.field.sid.label",
+ },
+ "connection_modal.field.sid.required": {
+ aliasKey: "connection_modal.field.sid.required",
+ },
+ "connection_modal.field.sid.placeholder": {
+ aliasKey: "connection_modal.field.sid.placeholder",
+ },
};
export const resolveLanguage = (
diff --git a/internal/app/db_context.go b/internal/app/db_context.go
index 2ee16799..ce531b6b 100644
--- a/internal/app/db_context.go
+++ b/internal/app/db_context.go
@@ -45,7 +45,7 @@ func normalizeRunConfig(config connection.ConnectionConfig, dbName string) conne
runConfig = runConfig.WithRuntimeDatabaseOverride(name)
}
default:
- // oracle: dbName 表示 schema/owner,不能覆盖 config.Database(服务名)
+ // oracle: dbName 表示 schema/owner,不能覆盖 config.Database(服务名)或 SID(SID 模式)
// sqlite: 无需设置 Database
// 其他 custom: 语义不明确,避免污染缓存 key
}
diff --git a/internal/app/navicat_ncx_import.go b/internal/app/navicat_ncx_import.go
index cf9c0380..03e31be3 100644
--- a/internal/app/navicat_ncx_import.go
+++ b/internal/app/navicat_ncx_import.go
@@ -146,7 +146,10 @@ func parseNavicatNCXConnectionWithText(item navicatNCXConnection, text navicatTe
config.Database = strings.TrimSpace(item.TNS)
}
if configType == "oracle" && strings.EqualFold(strings.TrimSpace(item.OraServiceNameType), "SID") && strings.TrimSpace(config.Database) != "" {
+ // SID 模式:SID 值仅存于 ConnectionParams(go-ora 的 SID 查询参数),
+ // Database(服务名)置空避免 DSN path 冗余,语义与内置连接表单一致。
config.ConnectionParams = "SID=" + strings.TrimSpace(config.Database)
+ config.Database = ""
}
}
diff --git a/internal/app/navicat_ncx_import_test.go b/internal/app/navicat_ncx_import_test.go
index 7236ed08..c0fa9b2a 100644
--- a/internal/app/navicat_ncx_import_test.go
+++ b/internal/app/navicat_ncx_import_test.go
@@ -239,6 +239,9 @@ func TestImportConnectionsPayloadNavicatNCXMapsOracleSIDAndRedisDB(t *testing.T)
if oracleConn.Config.Type != "oracle" || oracleConn.Config.ConnectionParams != "SID=ORCL" {
t.Fatalf("expected oracle SID connection params, got %#v", oracleConn.Config)
}
+ if oracleConn.Config.Database != "" {
+ t.Fatalf("expected oracle SID import to leave Database empty (SID only in ConnectionParams), got %q", oracleConn.Config.Database)
+ }
resolvedOracle, err := app.resolveConnectionSecrets(oracleConn.Config)
if err != nil {
t.Fatalf("resolveConnectionSecrets for oracle returned error: %v", err)
diff --git a/internal/db/dsn_test.go b/internal/db/dsn_test.go
index 8c4953ce..5adf2cee 100644
--- a/internal/db/dsn_test.go
+++ b/internal/db/dsn_test.go
@@ -263,6 +263,79 @@ func TestOracleDSN_EscapesUserAndPassword(t *testing.T) {
}
}
+func TestOracleDSN_SIDModeOmitsPathAndCarriesSIDParam(t *testing.T) {
+ o := &OracleDB{}
+ cfg := connection.ConnectionConfig{
+ Type: "oracle",
+ Host: "127.0.0.1",
+ Port: 1521,
+ User: "system",
+ Password: "secret",
+ ConnectionParams: "SID=ORCL",
+ }
+
+ if !isOracleSIDMode(cfg) {
+ t.Fatal("expected isOracleSIDMode to be true when SID param present")
+ }
+ if got := oracleConnectionSID(cfg); got != "ORCL" {
+ t.Fatalf("oracleConnectionSID = %q, want ORCL", got)
+ }
+
+ dsn := o.getDSN(cfg)
+ parsed, err := url.Parse(dsn)
+ if err != nil {
+ t.Fatalf("parse dsn failed: %v", err)
+ }
+ if strings.Trim(parsed.EscapedPath(), "/") != "" {
+ t.Fatalf("SID 模式不应在 URL path 携带服务名,path=%q dsn=%s", parsed.EscapedPath(), dsn)
+ }
+ if got := parsed.Query().Get("SID"); got != "ORCL" {
+ t.Fatalf("SID 参数 = %q, want ORCL(dsn=%s)", got, dsn)
+ }
+ if parsed.Query().Get("PREFETCH_ROWS") == "" {
+ t.Fatalf("SID 模式应保留默认驱动参数,dsn=%s", dsn)
+ }
+}
+
+func TestOracleDSN_SIDModeOverridesLegacyDatabaseField(t *testing.T) {
+ o := &OracleDB{}
+ // 兼容历史数据:Navicat 导入的旧 SID 连接同时存在 Database 与 SID 参数,
+ // SID 优先且 path 不携带服务名,避免冗余与日志误读。
+ cfg := connection.ConnectionConfig{
+ Type: "oracle",
+ Host: "db.example.com",
+ Port: 1521,
+ User: "system",
+ Password: "secret",
+ Database: "ORCL",
+ ConnectionParams: "SID=ORCL",
+ }
+
+ dsn := o.getDSN(cfg)
+ parsed, err := url.Parse(dsn)
+ if err != nil {
+ t.Fatalf("parse dsn failed: %v", err)
+ }
+ if strings.Trim(parsed.EscapedPath(), "/") != "" {
+ t.Fatalf("SID 模式不应在 URL path 携带 Database,path=%q dsn=%s", parsed.EscapedPath(), dsn)
+ }
+ if got := parsed.Query().Get("SID"); got != "ORCL" {
+ t.Fatalf("SID 参数 = %q, want ORCL(dsn=%s)", got, dsn)
+ }
+}
+
+func TestOracleSIDParamParsingIsCaseInsensitive(t *testing.T) {
+ for _, raw := range []string{"sid=ORCL", "Sid=ORCL", "SID=ORCL", "SID =ORCL"} {
+ cfg := connection.ConnectionConfig{
+ Type: "oracle",
+ ConnectionParams: raw,
+ }
+ if got := oracleConnectionSID(cfg); got != "ORCL" {
+ t.Fatalf("oracleConnectionSID(%q) = %q, want ORCL", raw, got)
+ }
+ }
+}
+
func TestDamengDSN_KeepsRawPasswordForDriverParser(t *testing.T) {
d := &DamengDB{}
cfg := connection.ConnectionConfig{
diff --git a/internal/db/oracle_dsn_test.go b/internal/db/oracle_dsn_test.go
index 98bdaa4d..0d2be73d 100644
--- a/internal/db/oracle_dsn_test.go
+++ b/internal/db/oracle_dsn_test.go
@@ -111,13 +111,27 @@ func TestOracleDSNLogSummaryDoesNotExposePassword(t *testing.T) {
if strings.Contains(got, "top-secret") || strings.Contains(got, "sys@tenant") {
t.Fatalf("summary should not expose credentials, got %q", got)
}
- for _, want := range []string{"服务名=ORCLPDB1", "DBA_PRIVILEGE=SYSDBA", "AUTH_TYPE=NORMAL"} {
+ for _, want := range []string{"连接模式=服务名", "服务名=ORCLPDB1", "DBA_PRIVILEGE=SYSDBA", "AUTH_TYPE=NORMAL"} {
if !strings.Contains(got, want) {
t.Fatalf("expected summary to contain %q, got %q", want, got)
}
}
}
+func TestOracleDSNLogSummaryUsesEffectiveSIDOverLegacyDatabase(t *testing.T) {
+ dsn := "oracle://sys:top-secret@127.0.0.1:1521?SID=ORCL&DBA+PRIVILEGE=SYSDBA"
+ got := oracleDSNLogSummary(connection.ConnectionConfig{Database: "OLD_SERVICE"}, dsn)
+
+ for _, want := range []string{"连接模式=SID", "SID=ORCL", "DBA_PRIVILEGE=SYSDBA"} {
+ if !strings.Contains(got, want) {
+ t.Fatalf("expected summary to contain %q, got %q", want, got)
+ }
+ }
+ if strings.Contains(got, "OLD_SERVICE") || strings.Contains(got, "top-secret") {
+ t.Fatalf("summary should use the effective SID without exposing stale or secret values, got %q", got)
+ }
+}
+
func TestAnnotateOracleValidationErrorAddsClosedConnectionHint(t *testing.T) {
t.Parallel()
diff --git a/internal/db/oracle_impl.go b/internal/db/oracle_impl.go
index 8c6f0c39..ffa3757c 100644
--- a/internal/db/oracle_impl.go
+++ b/internal/db/oracle_impl.go
@@ -41,17 +41,36 @@ func oracleRuntimeError(key string, params map[string]any) error {
return fmt.Errorf("%s", localizedDriverRuntimeText(key, params))
}
+// oracleConnectionSID 解析连接配置(ConnectionParams / URI)中的 SID 参数。
+// SID 与 Service Name 是 Oracle 两种互斥的连接定位方式:go-ora 驱动在
+// CONNECT_DATA 中优先使用 SID(configurations/connect_config.go),因此
+// SID 模式只需把 SID 值放入 DSN 查询参数,Database(服务名)可留空。
+func oracleConnectionSID(config connection.ConnectionConfig) string {
+ values := url.Values{}
+ mergeConnectionParamsFromConfigWithAllowlist(values, config, oracleConnectionParamNames, "oracle")
+ return oracleQueryValue(values, "SID")
+}
+
+// isOracleSIDMode 报告连接是否以 SID 模式连接(存在 SID 参数时优先于服务名)。
+func isOracleSIDMode(config connection.ConnectionConfig) bool {
+ return oracleConnectionSID(config) != ""
+}
+
func (o *OracleDB) getDSN(config connection.ConnectionConfig) string {
- // oracle://user:pass@host:port/service_name
+ // 服务名模式:oracle://user:pass@host:port/service_name
+ // SID 模式:oracle://user:pass@host:port/?SID=sid(go-ora 驱动据此组装 (SID=...))
database := strings.TrimSpace(config.Database)
+ sid := oracleConnectionSID(config)
u := &url.URL{
Scheme: "oracle",
Host: net.JoinHostPort(config.Host, strconv.Itoa(config.Port)),
- Path: "/" + database,
+ }
+ if sid == "" {
+ u.Path = "/" + database
+ u.RawPath = "/" + url.PathEscape(database)
}
u.User = url.UserPassword(config.User, config.Password)
- u.RawPath = "/" + url.PathEscape(database)
q := url.Values{}
switch normalizedSSLMode(config) {
case sslModeRequired:
@@ -97,18 +116,28 @@ func oracleDSNLogSummary(config connection.ConnectionConfig, dsn string) string
}
params = parsed.Query()
}
- if serviceName == "" {
- serviceName = "(未配置)"
+ sid := oracleQueryValue(params, "SID")
+ mode := "服务名"
+ targetLabel := "服务名"
+ targetValue := serviceName
+ if sid != "" {
+ mode = "SID"
+ targetLabel = "SID"
+ targetValue = sid
}
- return fmt.Sprintf("服务名=%s CONNECT_TIMEOUT=%s READ_TIMEOUT=%s SSL=%s SSL_VERIFY=%s AUTH_TYPE=%s DBA_PRIVILEGE=%s SID=%s",
- serviceName,
+ if targetValue == "" {
+ targetValue = "(未配置)"
+ }
+ return fmt.Sprintf("连接模式=%s %s=%s CONNECT_TIMEOUT=%s READ_TIMEOUT=%s SSL=%s SSL_VERIFY=%s AUTH_TYPE=%s DBA_PRIVILEGE=%s",
+ mode,
+ targetLabel,
+ targetValue,
oracleQueryValueOrDefault(params, "CONNECT TIMEOUT"),
oracleQueryValueOrDefault(params, "READ TIMEOUT"),
oracleQueryValueOrDefault(params, "SSL"),
oracleQueryValueOrDefault(params, "SSL VERIFY"),
oracleQueryValueOrDefault(params, "AUTH TYPE"),
oracleQueryValueOrDefault(params, "DBA PRIVILEGE"),
- oracleQueryValueOrDefault(params, "SID"),
)
}
@@ -120,7 +149,7 @@ func annotateOracleValidationError(err error) error {
if !strings.Contains(message, "use of closed network connection") {
return err
}
- return fmt.Errorf("%w(Oracle 连接在验证阶段被服务端关闭或被驱动超时中断;请检查监听端口是否为 Oracle 协议端口、Service Name 是否正确、认证参数如 DBA_PRIVILEGE/AUTH_TYPE 是否匹配)", err)
+ return fmt.Errorf("%w(Oracle 连接在验证阶段被服务端关闭或被驱动超时中断;请检查监听端口是否为 Oracle 协议端口、服务名(Service Name)或 SID 是否正确、认证参数如 DBA_PRIVILEGE/AUTH_TYPE 是否匹配)", err)
}
func (o *OracleDB) Connect(config connection.ConnectionConfig) (err error) {
@@ -133,8 +162,9 @@ func (o *OracleDB) Connect(config connection.ConnectionConfig) (err error) {
runConfig := config
serviceName := strings.TrimSpace(config.Database)
- if serviceName == "" {
- return fmt.Errorf("Oracle 连接缺少服务名(Service Name),请在连接配置中填写,例如 ORCLPDB1")
+ sid := oracleConnectionSID(config)
+ if serviceName == "" && sid == "" {
+ return fmt.Errorf("Oracle 连接缺少服务名(Service Name)或 SID,请在连接配置中填写,例如 ORCLPDB1(服务名)或 ORCL(SID)")
}
if config.UseSSH {
diff --git a/shared/i18n/de-DE.json b/shared/i18n/de-DE.json
index 45ff4ef4..21bddb65 100644
--- a/shared/i18n/de-DE.json
+++ b/shared/i18n/de-DE.json
@@ -3391,7 +3391,13 @@
"connection_modal.field.replica_username_optional": "Replica-Benutzername (optional)",
"connection_modal.field.save_password": "Passwort speichern",
"connection_modal.field.serviceName.placeholder": "Zum Beispiel: ORCLPDB1",
+ "connection_modal.field.oracle_mode.label": "Verbindungsmodus",
+ "connection_modal.field.oracle_mode.service": "Servicename",
+ "connection_modal.field.oracle_mode.sid": "SID",
"connection_modal.field.service_name": "Servicename",
+ "connection_modal.field.sid.label": "SID",
+ "connection_modal.field.sid.required": "SID eingeben",
+ "connection_modal.field.sid.placeholder": "z. B. ORCL",
"connection_modal.field.ssh_host": "SSH-Host",
"connection_modal.field.ssh_password": "SSH-Passwort",
"connection_modal.field.ssh_user": "SSH-Benutzer",
diff --git a/shared/i18n/en-US.json b/shared/i18n/en-US.json
index 23e02e60..f6943f8d 100644
--- a/shared/i18n/en-US.json
+++ b/shared/i18n/en-US.json
@@ -3391,7 +3391,13 @@
"connection_modal.field.replica_username_optional": "Replica username (optional)",
"connection_modal.field.save_password": "Save password",
"connection_modal.field.serviceName.placeholder": "For example: ORCLPDB1",
+ "connection_modal.field.oracle_mode.label": "Connection mode",
+ "connection_modal.field.oracle_mode.service": "Service name",
+ "connection_modal.field.oracle_mode.sid": "SID",
"connection_modal.field.service_name": "Service name",
+ "connection_modal.field.sid.label": "SID",
+ "connection_modal.field.sid.required": "Please enter the SID",
+ "connection_modal.field.sid.placeholder": "For example: ORCL",
"connection_modal.field.ssh_host": "SSH host",
"connection_modal.field.ssh_password": "SSH password",
"connection_modal.field.ssh_user": "SSH user",
diff --git a/shared/i18n/ja-JP.json b/shared/i18n/ja-JP.json
index 06e56ab4..afa651c9 100644
--- a/shared/i18n/ja-JP.json
+++ b/shared/i18n/ja-JP.json
@@ -3391,7 +3391,13 @@
"connection_modal.field.replica_username_optional": "レプリカユーザー名(任意)",
"connection_modal.field.save_password": "パスワードを保存",
"connection_modal.field.serviceName.placeholder": "例: ORCLPDB1",
+ "connection_modal.field.oracle_mode.label": "接続モード",
+ "connection_modal.field.oracle_mode.service": "サービス名",
+ "connection_modal.field.oracle_mode.sid": "SID",
"connection_modal.field.service_name": "サービス名",
+ "connection_modal.field.sid.label": "SID",
+ "connection_modal.field.sid.required": "SID を入力してください",
+ "connection_modal.field.sid.placeholder": "例:ORCL",
"connection_modal.field.ssh_host": "SSH ホスト",
"connection_modal.field.ssh_password": "SSH パスワード",
"connection_modal.field.ssh_user": "SSH ユーザー",
diff --git a/shared/i18n/messages.ts b/shared/i18n/messages.ts
index 6ee203dc..ee999757 100644
--- a/shared/i18n/messages.ts
+++ b/shared/i18n/messages.ts
@@ -289,6 +289,12 @@ export const messages: Record> = {
"connection.modal.field.oceanBaseServiceName.help":
"Oracle 租户必须填写监听器注册的 SERVICE_NAME;用户名仍按 OceanBase 租户格式填写。",
"connection.modal.field.serviceName.placeholder": "例如:ORCLPDB1",
+ "connection.modal.field.oracleMode.label": "连接模式",
+ "connection.modal.field.oracleMode.service": "服务名称",
+ "connection.modal.field.oracleMode.sid": "SID",
+ "connection.modal.field.sid.label": "SID",
+ "connection.modal.field.sid.required": "请输入 SID",
+ "connection.modal.field.sid.placeholder": "例如:ORCL",
"connection.modal.jvm.unsupportedMode.saveTest":
"当前连接包含未支持的 JVM 模式;请先调整为 JMX、Endpoint 或 Agent 后再测试或保存",
"connection.modal.jvm.unsupportedTransport.saveTest":
@@ -1171,6 +1177,12 @@ export const messages: Record> = {
"connection.modal.field.oceanBaseServiceName.help":
"Oracle tenants require the SERVICE_NAME registered with the listener. Keep using the OceanBase tenant format for the username.",
"connection.modal.field.serviceName.placeholder": "For example: ORCLPDB1",
+ "connection.modal.field.oracleMode.label": "Connection mode",
+ "connection.modal.field.oracleMode.service": "Service name",
+ "connection.modal.field.oracleMode.sid": "SID",
+ "connection.modal.field.sid.label": "SID",
+ "connection.modal.field.sid.required": "Please enter the SID",
+ "connection.modal.field.sid.placeholder": "For example: ORCL",
"connection.modal.jvm.unsupportedMode.saveTest":
"This connection contains unsupported JVM modes. Change them to JMX, Endpoint, or Agent before testing or saving.",
"connection.modal.jvm.unsupportedTransport.saveTest":
diff --git a/shared/i18n/ru-RU.json b/shared/i18n/ru-RU.json
index a2ba1883..1232d9e5 100644
--- a/shared/i18n/ru-RU.json
+++ b/shared/i18n/ru-RU.json
@@ -3391,7 +3391,13 @@
"connection_modal.field.replica_username_optional": "Имя пользователя реплики (необязательно)",
"connection_modal.field.save_password": "Сохранить пароль",
"connection_modal.field.serviceName.placeholder": "Например: ORCLPDB1",
+ "connection_modal.field.oracle_mode.label": "Режим подключения",
+ "connection_modal.field.oracle_mode.service": "Имя службы",
+ "connection_modal.field.oracle_mode.sid": "SID",
"connection_modal.field.service_name": "Имя сервиса",
+ "connection_modal.field.sid.label": "SID",
+ "connection_modal.field.sid.required": "Введите SID",
+ "connection_modal.field.sid.placeholder": "например ORCL",
"connection_modal.field.ssh_host": "SSH-хост",
"connection_modal.field.ssh_password": "SSH-пароль",
"connection_modal.field.ssh_user": "SSH-пользователь",
diff --git a/shared/i18n/zh-CN.json b/shared/i18n/zh-CN.json
index 00e31339..469e5a55 100644
--- a/shared/i18n/zh-CN.json
+++ b/shared/i18n/zh-CN.json
@@ -3391,7 +3391,13 @@
"connection_modal.field.replica_username_optional": "从库用户名(可选)",
"connection_modal.field.save_password": "保存密码",
"connection_modal.field.serviceName.placeholder": "例如:ORCLPDB1",
+ "connection_modal.field.oracle_mode.label": "连接模式",
+ "connection_modal.field.oracle_mode.service": "服务名称",
+ "connection_modal.field.oracle_mode.sid": "SID",
"connection_modal.field.service_name": "服务名",
+ "connection_modal.field.sid.label": "SID",
+ "connection_modal.field.sid.required": "请输入 SID",
+ "connection_modal.field.sid.placeholder": "例如:ORCL",
"connection_modal.field.ssh_host": "SSH 主机",
"connection_modal.field.ssh_password": "SSH 密码",
"connection_modal.field.ssh_user": "SSH 用户",
diff --git a/shared/i18n/zh-TW.json b/shared/i18n/zh-TW.json
index 87412019..7682466a 100644
--- a/shared/i18n/zh-TW.json
+++ b/shared/i18n/zh-TW.json
@@ -3391,7 +3391,13 @@
"connection_modal.field.replica_username_optional": "從庫使用者名稱(選填)",
"connection_modal.field.save_password": "儲存密碼",
"connection_modal.field.serviceName.placeholder": "例如:ORCLPDB1",
+ "connection_modal.field.oracle_mode.label": "連線模式",
+ "connection_modal.field.oracle_mode.service": "服務名稱",
+ "connection_modal.field.oracle_mode.sid": "SID",
"connection_modal.field.service_name": "服務名稱",
+ "connection_modal.field.sid.label": "SID",
+ "connection_modal.field.sid.required": "請輸入 SID",
+ "connection_modal.field.sid.placeholder": "例如:ORCL",
"connection_modal.field.ssh_host": "SSH 主機",
"connection_modal.field.ssh_password": "SSH 密碼",
"connection_modal.field.ssh_user": "SSH 使用者",