From 6b389dbdfa1b1048ad60f165b1848bbe37566f58 Mon Sep 17 00:00:00 2001 From: YangYuS8 Date: Thu, 25 Jun 2026 12:38:11 +0800 Subject: [PATCH] fix(app): import SQLite file path from Navicat NCX (#1773) --- .../src/lib/__tests__/navicatImport.spec.ts | 87 +++++++++++++++++++ apps/desktop/src/lib/navicatImport.ts | 13 ++- 2 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/navicatImport.spec.ts diff --git a/apps/desktop/src/lib/__tests__/navicatImport.spec.ts b/apps/desktop/src/lib/__tests__/navicatImport.spec.ts new file mode 100644 index 0000000000..b99b54c556 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/navicatImport.spec.ts @@ -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(//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(` + +`); + + 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(` + +`); + + 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(` + +`); + + 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(` + +`); + + expect(connection?.db_type).toBe("postgres"); + expect(connection?.host).toBe("db.example.test"); + expect(connection?.database).toBe("appdb"); + expect(connection?.port).toBe(15432); + }); +}); diff --git a/apps/desktop/src/lib/navicatImport.ts b/apps/desktop/src/lib/navicatImport.ts index fb10dd86ca..ca0712b912 100644 --- a/apps/desktop/src/lib/navicatImport.ts +++ b/apps/desktop/src/lib/navicatImport.ts @@ -54,6 +54,10 @@ function getAny(values: Record, keys: string[]) { return ""; } +function getSqlitePath(values: Record) { + 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