mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
fix(xugu): require database before connecting
This commit is contained in:
@@ -1384,6 +1384,9 @@ func (s *server) validateConnection() error {
|
||||
}
|
||||
|
||||
func openDB(params connectParams) (*sql.DB, error) {
|
||||
if configuredDatabaseName(params) == "" {
|
||||
return nil, errors.New("XuguDB requires an existing database name; set Database or include DB in the connection string")
|
||||
}
|
||||
dsn := buildDSN(params)
|
||||
db, err := sql.Open("xugu", dsn)
|
||||
if err != nil {
|
||||
@@ -1920,13 +1923,47 @@ func isXuguMissingOnNullColumnError(err error) bool {
|
||||
}
|
||||
|
||||
func xuguDSNValue(dsn string, key string) string {
|
||||
for _, part := range strings.Split(dsn, ";") {
|
||||
var result string
|
||||
for _, part := range splitXuguDSNSegments(dsn) {
|
||||
name, value, ok := strings.Cut(part, "=")
|
||||
if ok && strings.EqualFold(strings.TrimSpace(name), key) {
|
||||
return strings.TrimSpace(value)
|
||||
value = strings.TrimSpace(value)
|
||||
if len(value) >= 2 && value[0] == '\'' && value[len(value)-1] == '\'' {
|
||||
value = strings.ReplaceAll(value[1:len(value)-1], "''", "'")
|
||||
}
|
||||
result = strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return result
|
||||
}
|
||||
|
||||
func splitXuguDSNSegments(dsn string) []string {
|
||||
var segments []string
|
||||
var current strings.Builder
|
||||
inQuotes := false
|
||||
|
||||
for index := 0; index < len(dsn); index++ {
|
||||
character := dsn[index]
|
||||
if character == '\'' {
|
||||
current.WriteByte(character)
|
||||
if inQuotes && index+1 < len(dsn) && dsn[index+1] == '\'' {
|
||||
current.WriteByte(dsn[index+1])
|
||||
index++
|
||||
} else {
|
||||
inQuotes = !inQuotes
|
||||
}
|
||||
continue
|
||||
}
|
||||
if character == ';' && !inQuotes {
|
||||
segments = append(segments, current.String())
|
||||
current.Reset()
|
||||
continue
|
||||
}
|
||||
current.WriteByte(character)
|
||||
}
|
||||
|
||||
segments = append(segments, current.String())
|
||||
return segments
|
||||
}
|
||||
|
||||
func (s *server) listSchemas() ([]string, error) {
|
||||
|
||||
@@ -452,6 +452,34 @@ func TestBuildDSNUsesConnectionFields(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenDBRejectsMissingDatabaseBeforeOpeningDriver(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
params connectParams
|
||||
}{
|
||||
{name: "empty structured database", params: connectParams{Host: "db.example.com", Username: "app"}},
|
||||
{name: "whitespace structured database", params: connectParams{Host: "db.example.com", Database: " \t ", Username: "app"}},
|
||||
{name: "empty native DSN database", params: connectParams{ConnectionString: "IP=db.example.com;DB=;User=app"}},
|
||||
{name: "quoted whitespace native DSN database", params: connectParams{ConnectionString: "IP=db.example.com;DB=' ';User=app"}},
|
||||
{name: "URL without database", params: connectParams{ConnectionString: "xugu://app:secret@db.example.com:5138/"}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
db, err := openDB(test.params)
|
||||
if err == nil {
|
||||
if db != nil {
|
||||
_ = db.Close()
|
||||
}
|
||||
t.Fatal("expected missing database to be rejected before opening the driver")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "XuguDB requires an existing database name") {
|
||||
t.Fatalf("unexpected missing-database error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDSNUsesDefaultPort(t *testing.T) {
|
||||
dsn := buildDSN(connectParams{
|
||||
Host: "db.example.com",
|
||||
@@ -707,6 +735,9 @@ func TestConfiguredDatabaseName(t *testing.T) {
|
||||
{params: connectParams{ConnectionString: "xugu://user:secret@db.example.com:5138/demo"}, want: "demo"},
|
||||
{params: connectParams{ConnectionString: "jdbc:xugu://db.example.com:5138/reporting"}, want: "reporting"},
|
||||
{params: connectParams{ConnectionString: "IP=db.example.com;DB=SYSTEM;User=SYSDBA;PWD=secret"}, want: "SYSTEM"},
|
||||
{params: connectParams{ConnectionString: "IP=db.example.com;DB='shop;east';User=APP"}, want: "shop;east"},
|
||||
{params: connectParams{ConnectionString: "IP=db.example.com;DB='sales''east';User=APP"}, want: "sales'east"},
|
||||
{params: connectParams{ConnectionString: "IP=db.example.com;DB=' ';User=APP"}, want: ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -69,6 +69,7 @@ import {
|
||||
import type { PluginCenterFocus } from "@/lib/plugins/pluginCenterNavigation";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { applyMeilisearchBasePathToExternalConfig, applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnectionUrl } from "@/lib/connection/connectionUrl";
|
||||
import { hasXuguConnectionDatabase } from "@/lib/connection/xuguDatabase";
|
||||
import { DEFAULT_QUERY_TIMEOUT_SECS, MAX_CONNECT_TIMEOUT_SECS, MAX_QUERY_TIMEOUT_SECS } from "@/lib/connection/timeoutLimits";
|
||||
import { buildOracleTnsConnectionString, normalizeOracleTnsAdminPath, parseOracleTnsConnectionString } from "@/lib/connection/oracleTnsConnection";
|
||||
import { connectionDeepLinkServiceHydrationValue, parseConnectionDeepLink, parseServiceConnectionUrl, type ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink";
|
||||
@@ -2853,6 +2854,7 @@ const databaseLabel = computed(() => {
|
||||
|
||||
const databasePlaceholder = computed(() => {
|
||||
if (form.value.db_type === "oracle" && form.value.oracle_connection_type === "tns") return t("connection.oracleTnsAliasPlaceholder");
|
||||
if (form.value.db_type === "xugu") return t("connection.databasePlaceholderRequired");
|
||||
if (form.value.db_type === "kingbase") return t("connection.databasePlaceholderRequired");
|
||||
const fallback = defaultDatabaseForProfile();
|
||||
if (!fallback) return t("connection.databasePlaceholder");
|
||||
@@ -4180,6 +4182,12 @@ function connectionConfigForSubmit(id: string, generatedName = "", validatePlugi
|
||||
if (!config.name?.trim()) {
|
||||
config.name = generatedName.trim() || generateConnectionName();
|
||||
}
|
||||
if (config.db_type === "xugu") {
|
||||
config.database = config.database?.trim() || undefined;
|
||||
if (!hasXuguConnectionDatabase(config.database, config.connection_string)) {
|
||||
throw new Error(t("connection.xuguDatabaseRequired"));
|
||||
}
|
||||
}
|
||||
if (config.db_type === "kingbase") {
|
||||
config.database = config.database?.trim() || undefined;
|
||||
if (!config.database) {
|
||||
|
||||
@@ -573,6 +573,7 @@ export default withEnglishFallback({
|
||||
databasePlaceholderRequired: "Tələb olunur, mövcud verilənlər bazasını daxil edin",
|
||||
databasePlaceholderWithDefault: "İstəyə bağlı, standart olaraq {database}",
|
||||
kingbaseDatabaseRequired: "KingbaseES əlaqə qurmazdan və ya verilənlər bazası siyahısını yükləməzdən əvvəl mövcud verilənlər bazası tələb edir.",
|
||||
xuguDatabaseRequired: "XuguDB-yə qoşulmaq üçün əvvəlcə mövcud verilənlər bazası daxil edilməlidir.",
|
||||
defaultDatabase: "Standart verilənlər bazası",
|
||||
authDatabase: "Autentifikasiya verilənlər bazası",
|
||||
authDatabasePlaceholder: "İstəyə bağlı, adətən admin",
|
||||
|
||||
@@ -716,6 +716,7 @@ export default {
|
||||
databasePlaceholderRequired: "Required, enter an existing database",
|
||||
databasePlaceholderWithDefault: "Optional, defaults to {database}",
|
||||
kingbaseDatabaseRequired: "KingbaseES requires an existing database before connecting or loading the database list.",
|
||||
xuguDatabaseRequired: "XuguDB requires an existing database before connecting.",
|
||||
defaultDatabase: "Default DB",
|
||||
authDatabase: "Auth DB",
|
||||
authDatabasePlaceholder: "Optional, often admin",
|
||||
|
||||
@@ -708,6 +708,7 @@ export default withEnglishFallback({
|
||||
databasePlaceholderRequired: "Obligatorio, ingresa una base de datos existente",
|
||||
databasePlaceholderWithDefault: "Opcional, por defecto {database}",
|
||||
kingbaseDatabaseRequired: "KingbaseES requiere una base de datos existente antes de conectar o cargar la lista de bases de datos.",
|
||||
xuguDatabaseRequired: "XuguDB requiere una base de datos existente antes de conectarse.",
|
||||
defaultDatabase: "BD predeterminada",
|
||||
authDatabase: "BD de autenticación",
|
||||
authDatabasePlaceholder: "Opcional, normalmente admin",
|
||||
|
||||
@@ -706,6 +706,7 @@ export default withEnglishFallback({
|
||||
databasePlaceholderRequired: "Obbligatorio, inserisci un database esistente",
|
||||
databasePlaceholderWithDefault: "Opzionale, predefinito {database}",
|
||||
kingbaseDatabaseRequired: "KingbaseES richiede un database esistente prima di connettersi o caricare l'elenco dei database.",
|
||||
xuguDatabaseRequired: "XuguDB richiede un database esistente prima di connettersi.",
|
||||
defaultDatabase: "DB Predefinito",
|
||||
authDatabase: "DB di Autenticazione",
|
||||
authDatabasePlaceholder: "Opzionale, spesso admin",
|
||||
|
||||
@@ -706,6 +706,7 @@ export default withEnglishFallback({
|
||||
databasePlaceholderRequired: "必須、既存のデータベースを入力",
|
||||
databasePlaceholderWithDefault: "任意、デフォルトは {database}",
|
||||
kingbaseDatabaseRequired: "KingbaseES は接続またはデータベース一覧の読み込み前に、既存のデータベース名が必要です。",
|
||||
xuguDatabaseRequired: "XuguDB に接続するには、既存のデータベース名が必要です。",
|
||||
defaultDatabase: "デフォルトDB",
|
||||
authDatabase: "認証DB",
|
||||
authDatabasePlaceholder: "任意、通常は admin",
|
||||
|
||||
@@ -708,6 +708,7 @@ export default withEnglishFallback({
|
||||
databasePlaceholderRequired: "필수, 기존 데이터베이스 입력",
|
||||
databasePlaceholderWithDefault: "선택, 기본값 {database}",
|
||||
kingbaseDatabaseRequired: "KingbaseES는 연결이나 데이터베이스 목록을 불러오기 전에 기존 데이터베이스가 필요합니다.",
|
||||
xuguDatabaseRequired: "XuguDB에 연결하려면 기존 데이터베이스가 필요합니다.",
|
||||
defaultDatabase: "기본 DB",
|
||||
authDatabase: "인증 DB",
|
||||
authDatabasePlaceholder: "선택, 보통 admin",
|
||||
|
||||
@@ -707,6 +707,7 @@ export default withEnglishFallback({
|
||||
databasePlaceholderRequired: "Obrigatório, informe um banco de dados existente",
|
||||
databasePlaceholderWithDefault: "Opcional, padrão é {database}",
|
||||
kingbaseDatabaseRequired: "KingbaseES exige um banco de dados existente antes de conectar ou carregar a lista de bancos de dados.",
|
||||
xuguDatabaseRequired: "XuguDB exige um banco de dados existente antes de conectar.",
|
||||
defaultDatabase: "BD padrão",
|
||||
authDatabase: "BD de autenticação",
|
||||
authDatabasePlaceholder: "Opcional, geralmente admin",
|
||||
|
||||
@@ -575,6 +575,7 @@ export default withEnglishFallback({
|
||||
databasePlaceholderRequired: "Zorunlu, mevcut bir veritabanı girin",
|
||||
databasePlaceholderWithDefault: "İsteğe bağlı, varsayılan {database}",
|
||||
kingbaseDatabaseRequired: "KingbaseES bağlanmadan veya veritabanı listesini yüklemeden önce mevcut bir veritabanı ister.",
|
||||
xuguDatabaseRequired: "XuguDB'ye bağlanmadan önce mevcut bir veritabanı gerekir.",
|
||||
defaultDatabase: "Varsayılan VT",
|
||||
authDatabase: "Kimlik Doğrulama VT",
|
||||
authDatabasePlaceholder: "İsteğe bağlı, genellikle admin",
|
||||
|
||||
@@ -632,6 +632,7 @@ export default withEnglishFallback({
|
||||
databasePlaceholderRequired: "必填,请填写已存在的数据库",
|
||||
databasePlaceholderWithDefault: "可选,默认 {database}",
|
||||
kingbaseDatabaseRequired: "KingbaseES 需要先填写一个已存在的数据库后才能连接或加载数据库列表。",
|
||||
xuguDatabaseRequired: "虚谷 XuguDB 需要先填写一个已存在的数据库才能连接。",
|
||||
defaultDatabase: "默认库",
|
||||
authDatabase: "认证库",
|
||||
authDatabasePlaceholder: "可选,通常为 admin",
|
||||
|
||||
@@ -710,6 +710,7 @@ export default withEnglishFallback({
|
||||
databasePlaceholderRequired: "必填,請填寫已存在的資料庫",
|
||||
databasePlaceholderWithDefault: "可選,預設 {database}",
|
||||
kingbaseDatabaseRequired: "KingbaseES 需要先填寫一個已存在的資料庫後才能連線或載入資料庫清單。",
|
||||
xuguDatabaseRequired: "虛谷 XuguDB 需要先填寫一個已存在的資料庫才能連線。",
|
||||
defaultDatabase: "預設 DB",
|
||||
authDatabase: "驗證 DB",
|
||||
authDatabasePlaceholder: "可選,通常為 admin",
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("ConnectionDialog Xugu database requirement", () => {
|
||||
it("shows the existing required-database hint only for Xugu", () => {
|
||||
const placeholder = dialogSource.slice(dialogSource.indexOf("const databasePlaceholder = computed"), dialogSource.indexOf("const transportLayers"));
|
||||
expect(placeholder).toContain('if (form.value.db_type === "xugu") return t("connection.databasePlaceholderRequired")');
|
||||
expect(placeholder).toContain('if (form.value.db_type === "kingbase") return t("connection.databasePlaceholderRequired")');
|
||||
});
|
||||
|
||||
it("validates Xugu's effective database before connection testing or saving", () => {
|
||||
const submit = dialogSource.slice(dialogSource.indexOf("function connectionConfigForSubmit"), dialogSource.indexOf("function ", dialogSource.indexOf("function connectionConfigForSubmit") + 10));
|
||||
expect(submit).toContain('if (config.db_type === "xugu")');
|
||||
expect(submit).toContain("hasXuguConnectionDatabase(config.database, config.connection_string)");
|
||||
expect(submit).toContain('throw new Error(t("connection.xuguDatabaseRequired"))');
|
||||
expect(submit).toContain('if (config.db_type === "kingbase")');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { hasXuguConnectionDatabase, xuguDatabaseFromConnectionString } from "@/lib/connection/xuguDatabase";
|
||||
|
||||
describe("Xugu connection database validation", () => {
|
||||
it("accepts a database selected in the structured field", () => {
|
||||
expect(hasXuguConnectionDatabase(" XUGU_TEST_DB ", undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects a missing or whitespace-only database", () => {
|
||||
expect(hasXuguConnectionDatabase(undefined, undefined)).toBe(false);
|
||||
expect(hasXuguConnectionDatabase(" \t ", undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it.each(["xugu://user:password@host:5138/XUGU_TEST_DB", "jdbc:xugu://host:5138/XUGU_TEST_DB"])("accepts a database encoded in a Xugu URL: %s", (url) => {
|
||||
expect(xuguDatabaseFromConnectionString(url)).toBe("XUGU_TEST_DB");
|
||||
expect(hasXuguConnectionDatabase(undefined, url)).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts a database in a native Xugu DSN, including quoted separators", () => {
|
||||
expect(xuguDatabaseFromConnectionString("IP=db.example;DB='shop;east';User=app")).toBe("shop;east");
|
||||
expect(xuguDatabaseFromConnectionString("IP=db.example;DB='sales''east';User=app")).toBe("sales'east");
|
||||
});
|
||||
|
||||
it("rejects a missing or empty database in a native Xugu DSN", () => {
|
||||
expect(xuguDatabaseFromConnectionString("IP=db.example;User=app")).toBeUndefined();
|
||||
expect(xuguDatabaseFromConnectionString("IP=db.example;DB=;User=app")).toBeUndefined();
|
||||
expect(xuguDatabaseFromConnectionString("IP=db.example;DB=' ';User=app")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not treat another database's URL as a Xugu database", () => {
|
||||
expect(xuguDatabaseFromConnectionString("postgres://user:password@host:5432/app")).toBeUndefined();
|
||||
expect(hasXuguConnectionDatabase(undefined, "postgres://user:password@host:5432/app")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { parseConnectionUrl } from "@/lib/connection/connectionUrl";
|
||||
|
||||
function splitXuguDsnParts(value: string): string[] {
|
||||
const parts: string[] = [];
|
||||
let current = "";
|
||||
let inQuotes = false;
|
||||
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
const character = value[index];
|
||||
if (character === "'") {
|
||||
current += character;
|
||||
if (inQuotes && value[index + 1] === "'") {
|
||||
current += value[index + 1];
|
||||
index += 1;
|
||||
} else {
|
||||
inQuotes = !inQuotes;
|
||||
}
|
||||
} else if (character === ";" && !inQuotes) {
|
||||
parts.push(current);
|
||||
current = "";
|
||||
} else {
|
||||
current += character;
|
||||
}
|
||||
}
|
||||
|
||||
parts.push(current);
|
||||
return parts;
|
||||
}
|
||||
|
||||
function xuguDatabaseFromDsn(value: string): string | undefined {
|
||||
let database: string | undefined;
|
||||
for (const part of splitXuguDsnParts(value)) {
|
||||
const separator = part.indexOf("=");
|
||||
if (separator < 0 || part.slice(0, separator).trim().toLowerCase() !== "db") continue;
|
||||
|
||||
let candidate = part.slice(separator + 1).trim();
|
||||
if (candidate.startsWith("'") && candidate.endsWith("'")) {
|
||||
candidate = candidate.slice(1, -1).replaceAll("''", "'");
|
||||
}
|
||||
database = candidate.trim() || undefined;
|
||||
}
|
||||
return database;
|
||||
}
|
||||
|
||||
export function xuguDatabaseFromConnectionString(connectionString: string | undefined): string | undefined {
|
||||
const value = connectionString?.trim();
|
||||
if (!value) return undefined;
|
||||
|
||||
if (/^(?:ip|ips)\s*=/i.test(value)) {
|
||||
return xuguDatabaseFromDsn(value);
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = parseConnectionUrl(value, "xugu");
|
||||
return parsed.dbType === "xugu" ? parsed.database?.trim() || undefined : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function hasXuguConnectionDatabase(database: string | undefined, connectionString: string | undefined): boolean {
|
||||
return !!database?.trim() || !!xuguDatabaseFromConnectionString(connectionString);
|
||||
}
|
||||
Reference in New Issue
Block a user