feat(oracle): add TNS connections and trigger editing
This commit is contained in:
1 parent
9a5edae219
commit
3b004abb1e
23 files changed
+980
-53
No files matched your search
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildOracleTnsConnectionString, normalizeOracleTnsAdminPath, parseOracleTnsConnectionString } from "./oracleTnsConnection";
|
||||
|
||||
describe("Oracle TNS connection settings", () => {
|
||||
it("round-trips an alias and TNS_ADMIN directory", () => {
|
||||
const value = buildOracleTnsConnectionString("DBX_FAILOVER", "C:\\oracle network\\admin");
|
||||
|
||||
expect(parseOracleTnsConnectionString(value)).toEqual({
|
||||
alias: "DBX_FAILOVER",
|
||||
tnsAdmin: "C:\\oracle network\\admin",
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes a selected tnsnames.ora file to its parent directory", () => {
|
||||
expect(normalizeOracleTnsAdminPath("C:\\oracle\\network\\admin\\tnsnames.ora")).toBe("C:\\oracle\\network\\admin");
|
||||
expect(normalizeOracleTnsAdminPath("C:\\tnsnames.ora")).toBe("C:\\");
|
||||
expect(normalizeOracleTnsAdminPath("/opt/oracle/network/admin/tnsnames.ora")).toBe("/opt/oracle/network/admin");
|
||||
});
|
||||
|
||||
it("does not reinterpret service, SID, or descriptor JDBC URLs as TNS aliases", () => {
|
||||
expect(parseOracleTnsConnectionString("jdbc:oracle:thin:@//db.example.com:1521/ORCLPDB1")).toBeNull();
|
||||
expect(parseOracleTnsConnectionString("jdbc:oracle:thin:@db.example.com:1521:ORCL")).toBeNull();
|
||||
expect(parseOracleTnsConnectionString("jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(HOST=db.example.com)))")).toBeNull();
|
||||
});
|
||||
|
||||
it("ignores malformed encoded aliases instead of breaking connection editing", () => {
|
||||
expect(parseOracleTnsConnectionString("jdbc:oracle:thin:@DBX%ZZ?TNS_ADMIN=%2Fopt%2Foracle")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
export interface OracleTnsConnectionConfig {
|
||||
alias: string;
|
||||
tnsAdmin: string;
|
||||
}
|
||||
|
||||
const ORACLE_TNS_JDBC_PREFIX = "jdbc:oracle:thin:@";
|
||||
|
||||
export function parseOracleTnsConnectionString(value?: string): OracleTnsConnectionConfig | null {
|
||||
const source = value?.trim() || "";
|
||||
if (!source.toLowerCase().startsWith(ORACLE_TNS_JDBC_PREFIX)) return null;
|
||||
|
||||
const target = source.slice(ORACLE_TNS_JDBC_PREFIX.length);
|
||||
if (!target || target.startsWith("(") || target.startsWith("//") || target.includes(":")) return null;
|
||||
|
||||
const [rawAlias, rawQuery = ""] = target.split("?", 2);
|
||||
let alias = "";
|
||||
try {
|
||||
alias = decodeURIComponent(rawAlias).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const tnsAdmin = new URLSearchParams(rawQuery).get("TNS_ADMIN")?.trim() || "";
|
||||
if (!alias) return null;
|
||||
return { alias, tnsAdmin };
|
||||
}
|
||||
|
||||
export function buildOracleTnsConnectionString(alias: string, tnsAdmin: string): string {
|
||||
const normalizedAlias = alias.trim();
|
||||
const normalizedTnsAdmin = normalizeOracleTnsAdminPath(tnsAdmin);
|
||||
const query = new URLSearchParams({ TNS_ADMIN: normalizedTnsAdmin });
|
||||
return `${ORACLE_TNS_JDBC_PREFIX}${encodeURIComponent(normalizedAlias)}?${query.toString()}`;
|
||||
}
|
||||
|
||||
export function normalizeOracleTnsAdminPath(value: string): string {
|
||||
const path = value.trim();
|
||||
if (!path) return "";
|
||||
if (!/(^|[\\/])tnsnames\.ora$/i.test(path)) return path;
|
||||
|
||||
const parent = path.replace(/[\\/]tnsnames\.ora$/i, "");
|
||||
if (/^[A-Za-z]:$/.test(parent)) return `${parent}\\`;
|
||||
if (parent) return parent;
|
||||
return path.startsWith("\\") ? "\\" : "/";
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { isSchemaAware, isSingleDatabase } from "@/lib/database/databaseFeatureS
|
||||
import { extractIdentifierPartsAt, isSqlKeyword, sqlObjectNavigationTarget, type SqlObjectNavigationTarget, type SqlObjectNavigationType } from "@/lib/sql/sqlNavigation";
|
||||
import type { ActiveTabSidebarTarget } from "@/lib/sidebar/sidebarActiveTabTarget";
|
||||
import type { SqlCompletionTable } from "@/lib/sql/sqlCompletion";
|
||||
import { sqlSemanticDialectFor } from "@/lib/sql/semantic/dialect";
|
||||
import type { DatabaseType, QueryTab, TreeNode } from "@/types/database";
|
||||
|
||||
export interface QueryCursorTableCandidate {
|
||||
@@ -53,7 +54,9 @@ export function queryCursorTableCandidate(tab: QueryTab | undefined | null, data
|
||||
}
|
||||
|
||||
export function queryTableCandidateAtSqlPosition(input: QueryTableCandidateAtPositionInput): QueryCursorTableCandidate | null {
|
||||
const parts = extractQualifiedIdentifierPartsAt(input.sql, input.position).map((part) => part.value);
|
||||
const dialect = sqlSemanticDialectFor({ databaseType: input.databaseType });
|
||||
// View-data SQL quotes resolved names, so fold only unquoted input exactly as the database does before quoting it again.
|
||||
const parts = extractQualifiedIdentifierPartsAt(input.sql, input.position).map((part) => dialect.normalizeIdentifier(part.value, part.quoted));
|
||||
if (parts.length === 0) return null;
|
||||
|
||||
const tableName = parts[parts.length - 1];
|
||||
|
||||
Reference in new issue
Block a user