fix(app): import SQLite file path from Navicat NCX (#1773)

This commit is contained in:
YangYuS8
2026-06-25 12:38:11 +08:00
committed by GitHub
parent b367861fe1
commit 6b389dbdfa
2 changed files with 96 additions and 4 deletions
@@ -0,0 +1,87 @@
import { describe, expect, it } from "vitest";
import { parseNavicatConnections } from "../navicatImport";
class TestElement {
readonly tagName: string;
readonly attributes: { name: string; value: string }[];
readonly children: TestElement[] = [];
readonly textContent = "";
constructor(tagName: string, attributes: { name: string; value: string }[]) {
this.tagName = tagName;
this.attributes = attributes;
}
}
class TestDocument {
private readonly elements: TestElement[];
constructor(xml: string) {
this.elements = Array.from(xml.matchAll(/<Connection\b([\s\S]*?)\/>/gi)).map((match) => new TestElement("Connection", parseAttributes(match[1] || "")));
}
querySelector(selector: string) {
return selector === "parsererror" ? null : null;
}
querySelectorAll(selector: string) {
return selector === "*" ? this.elements : [];
}
}
class TestDOMParser {
parseFromString(xml: string) {
return new TestDocument(xml);
}
}
function parseAttributes(source: string) {
return Array.from(source.matchAll(/([^\s=]+)="([^"]*)"/g)).map((match) => ({ name: match[1] || "", value: match[2] || "" }));
}
if (!globalThis.DOMParser) {
globalThis.DOMParser = TestDOMParser as typeof DOMParser;
}
describe("parseNavicatConnections", () => {
it("imports SQLite DatabaseFile as both host and database", async () => {
const [connection] = await parseNavicatConnections(`<Connections>
<Connection ConnType="SQLite" Name="local-sqlite" DatabaseFile="C:\\Users\\Yang\\demo.db" />
</Connections>`);
expect(connection?.db_type).toBe("sqlite");
expect(connection?.host).toBe("C:\\Users\\Yang\\demo.db");
expect(connection?.database).toBe("C:\\Users\\Yang\\demo.db");
expect(connection?.port).toBe(0);
});
it("imports SQLite numeric ConnType file name as host", async () => {
const [connection] = await parseNavicatConnections(`<Connections>
<Connection ConnType="3" Name="sqlite-by-code" DatabaseFileName="/home/yang/demo.sqlite" />
</Connections>`);
expect(connection?.db_type).toBe("sqlite");
expect(connection?.host).toBe("/home/yang/demo.sqlite");
});
it("uses SQLite Database field as the file path", async () => {
const [connection] = await parseNavicatConnections(`<Connections>
<Connection ConnType="SQLite" Name="sqlite-database-field" Database="/tmp/app.data" />
</Connections>`);
expect(connection?.db_type).toBe("sqlite");
expect(connection?.host).toBe("/tmp/app.data");
expect(connection?.database).toBe("/tmp/app.data");
});
it("keeps non-SQLite host and database mapping unchanged", async () => {
const [connection] = await parseNavicatConnections(`<Connections>
<Connection ConnType="PostgreSQL" Name="pg" Host="db.example.test" Database="appdb" Port="15432" />
</Connections>`);
expect(connection?.db_type).toBe("postgres");
expect(connection?.host).toBe("db.example.test");
expect(connection?.database).toBe("appdb");
expect(connection?.port).toBe(15432);
});
});
+9 -4
View File
@@ -54,6 +54,10 @@ function getAny(values: Record<string, string>, keys: string[]) {
return "";
}
function getSqlitePath(values: Record<string, string>) {
return getAny(values, ["databaseFile", "databaseFileName", "databaseFilename", "filename", "fileName", "path", "databasePath", "dbPath", "dbFile", "sqliteFile", "sqlitePath", "database", "databaseName"]);
}
function truthyNavicatFlag(value: string) {
const normalized = value.trim().toLowerCase();
return ["1", "true", "yes", "y", "on", "checked"].includes(normalized);
@@ -145,7 +149,7 @@ function isConnectionCandidate(node: ParsedNode) {
const type = getAny(node.values, ["type", "connType", "connectionType", "databaseType", "driver"]);
const name = getAny(node.values, ["name", "connectionName", "connName", "caption", "title"]);
const host = getAny(node.values, ["host", "server", "hostname", "serverHost", "address"]);
const file = getAny(node.values, ["databaseFile", "filename", "path", "databasePath"]);
const file = getSqlitePath(node.values);
return !!(name || host || file) && !!(type || host || file);
}
@@ -174,9 +178,10 @@ async function parseConnection(node: ParsedNode): Promise<ConnectionConfig | nul
}
}
const name = getAny(node.values, ["name", "connectionName", "connName", "caption", "title"]) || getAny(node.values, ["host", "server", "hostname"]) || effectiveProfile.label;
const host = getAny(node.values, ["host", "server", "hostname", "serverHost", "address"]) || getAny(node.values, ["databaseFile", "filename", "path", "databasePath"]) || (effectiveProfile.dbType === "sqlite" ? "" : "127.0.0.1");
const database = getAny(node.values, ["database", "databaseName", "initialDatabase", "serviceName", "sid", "schema"]);
const sqlitePath = effectiveProfile.dbType === "sqlite" ? getSqlitePath(node.values) : "";
const name = getAny(node.values, ["name", "connectionName", "connName", "caption", "title"]) || getAny(node.values, ["host", "server", "hostname"]) || sqlitePath || effectiveProfile.label;
const host = sqlitePath || getAny(node.values, ["host", "server", "hostname", "serverHost", "address"]) || (effectiveProfile.dbType === "sqlite" ? "" : "127.0.0.1");
const database = effectiveProfile.dbType === "sqlite" ? sqlitePath : getAny(node.values, ["database", "databaseName", "initialDatabase", "serviceName", "sid", "schema"]);
const isOracleLike = effectiveProfile.dbType === "oracle" || effectiveProfile.dbType === "oceanbase-oracle";
const oracleConnectionType = isOracleLike && getAny(node.values, ["sid"]) ? "sid" : isOracleLike ? "service_name" : undefined;
const username = getAny(node.values, ["user", "username", "userName", "uid"]) || profile.user;