From 427d0c548be9980920407459154c547365b0558c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=95=A5=E9=83=BD=E4=B8=8D=E4=BC=9A?= <96872606+Driftlux@users.noreply.github.com> Date: Tue, 30 Jun 2026 21:56:47 +0800 Subject: [PATCH] fix(mysql): allow create database without selection --- .../__tests__/useSqlExecution.spec.ts | 49 +++++++++++++++++++ .../src/composables/useSqlExecution.ts | 19 +++++-- 2 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts diff --git a/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts b/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts new file mode 100644 index 0000000000..2c845ced34 --- /dev/null +++ b/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { requiresDatabaseSelection } from "../useSqlExecution"; +import type { ConnectionConfig, QueryTab } from "@/types/database"; + +function connection(dbType: ConnectionConfig["db_type"]): ConnectionConfig { + return { + id: "conn-1", + name: "Local", + db_type: dbType, + host: "localhost", + port: 3306, + username: "root", + password: "", + }; +} + +function queryTab(database = ""): QueryTab { + return { + id: "tab-1", + connectionId: "conn-1", + database, + schema: undefined, + title: "SQL", + sql: "", + mode: "query", + isDirty: false, + isExecuting: false, + isCancelling: false, + isExplaining: false, + }; +} + +describe("requiresDatabaseSelection", () => { + it("allows MySQL CREATE DATABASE to run without a selected database", () => { + expect(requiresDatabaseSelection(queryTab(), connection("mysql"), "CREATE DATABASE app_db")).toBe(false); + }); + + it("allows MySQL CREATE SCHEMA with options to run without a selected database", () => { + expect(requiresDatabaseSelection(queryTab(), connection("mysql"), "CREATE SCHEMA `app-db` DEFAULT CHARACTER SET utf8mb4")).toBe(false); + }); + + it("requires a database when a MySQL batch includes non-creation statements", () => { + expect(requiresDatabaseSelection(queryTab(), connection("mysql"), "CREATE DATABASE app_db; SELECT * FROM users")).toBe(true); + }); + + it("still requires a database for ordinary MySQL queries", () => { + expect(requiresDatabaseSelection(queryTab(), connection("mysql"), "SELECT * FROM users")).toBe(true); + }); +}); diff --git a/apps/desktop/src/composables/useSqlExecution.ts b/apps/desktop/src/composables/useSqlExecution.ts index 32963fb29a..664241800a 100644 --- a/apps/desktop/src/composables/useSqlExecution.ts +++ b/apps/desktop/src/composables/useSqlExecution.ts @@ -14,6 +14,7 @@ import { extractSqlParameters } from "@/lib/sqlParameters"; import type { ConnectionConfig, QueryTab } from "@/types/database"; const DANGER_RE = /^\s*(DROP|DELETE|TRUNCATE|ALTER|UPDATE|MERGE|REPLACE)\b/i; +const CONNECTION_LEVEL_CREATE_DATABASE_RE = /^\s*CREATE\s+(?:DATABASE|SCHEMA)\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"[\]\w$.-]+/i; export function stripSqlComments(sql: string): string { return sql @@ -72,7 +73,7 @@ export function useSqlExecution(deps: { const tab = deps.activeTab.value; const sql = await resolvedExecutableSql(sqlOverride); if (!tab || !sql.trim()) return; - if (requiresDatabaseSelection(tab, deps.activeConnection.value)) { + if (requiresDatabaseSelection(tab, deps.activeConnection.value, sql)) { deps.onMissingDatabase?.(); return; } @@ -118,7 +119,7 @@ export function useSqlExecution(deps: { sql ??= await resolvedExecutableSql(); const tab = deps.activeTab.value; if (!tab || !sql.trim()) return; - if (requiresDatabaseSelection(tab, deps.activeConnection.value)) { + if (requiresDatabaseSelection(tab, deps.activeConnection.value, sql)) { deps.onMissingDatabase?.(); return; } @@ -226,9 +227,21 @@ function supportsSqlTemplateParameters(connection: ConnectionConfig | undefined) return connection.db_type !== "redis" && connection.db_type !== "mongodb"; } -function requiresDatabaseSelection(tab: QueryTab, connection: ConnectionConfig | undefined): boolean { +function canExecuteWithoutSelectedDatabase(connection: ConnectionConfig, sql: string): boolean { + if (connection.db_type !== "mysql") return false; + const statements = stripSqlComments(sql) + .split(";") + .map((stmt) => stmt.trim()) + .filter(Boolean); + // Only connection-level database creation may bypass the selected-database guard. + if (statements.length !== 1) return false; + return CONNECTION_LEVEL_CREATE_DATABASE_RE.test(statements[0]); +} + +export function requiresDatabaseSelection(tab: QueryTab, connection: ConnectionConfig | undefined, sql = ""): boolean { if (tab.mode !== "query") return false; if (!connection || tab.database) return false; if (isSingleDatabase(connection.db_type)) return false; + if (canExecuteWithoutSelectedDatabase(connection, sql)) return false; return !["elasticsearch", "qdrant", "milvus", "weaviate", "chromadb", "zookeeper"].includes(connection.db_type); }