From 7d41e4123f666fe61cd07910a810442b15e47eda Mon Sep 17 00:00:00 2001 From: zipg Date: Tue, 21 Jul 2026 11:22:51 +0800 Subject: [PATCH] fix(mysql): improve create database permission hint --- .../sidebar/SidebarTreeRuntimeHost.vue | 17 +++++++--- apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/es.ts | 1 + apps/desktop/src/i18n/locales/it.ts | 1 + apps/desktop/src/i18n/locales/ja.ts | 1 + apps/desktop/src/i18n/locales/pt-BR.ts | 1 + apps/desktop/src/i18n/locales/zh-CN.ts | 1 + apps/desktop/src/i18n/locales/zh-TW.ts | 1 + .../database/createDatabaseErrorHints.spec.ts | 31 +++++++++++++++++++ .../lib/database/createDatabaseErrorHints.ts | 13 ++++++++ 10 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/database/createDatabaseErrorHints.spec.ts create mode 100644 apps/desktop/src/lib/database/createDatabaseErrorHints.ts diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index 4dd9eaa76..e6f484aca 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -91,6 +91,7 @@ import { dataTabOpenModeFromTreeClick, type DataTabOpenMode } from "@/lib/sideba import { isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut } from "@/lib/editor/keyboardShortcuts"; import { canRefreshDataTableFromSingleActivationDoubleClick, dataTableDoubleClickAction } from "@/lib/tabs/dataTabActivation"; import { attachedDatabaseNameFromPath, buildCreateDatabaseSql, buildDuckDbAttachDatabaseSql, buildSqliteAttachDatabaseSql, supportsCreateDatabaseCharset, uniqueAttachedDatabaseName } from "@/lib/database/createDatabaseSql"; +import { appendCreateDatabaseErrorHint } from "@/lib/database/createDatabaseErrorHints"; import { SQLITE_DATABASE_FILE_EXTENSIONS } from "@/lib/database/databaseFileDetection"; import { buildCreateSchemaSql, @@ -2572,24 +2573,30 @@ async function applyCreateDatabaseAuthorizationPlan() { if (!node.connectionId || !plan || createDatabaseAuthorizationApplying.value) return; createDatabaseAuthorizationApplying.value = true; try { + const config = connectionStore.getConfig(node.connectionId); const results = await executeWithProductionSqlGuard({ - connection: connectionStore.getConfig(node.connectionId), + connection: config, database: "", sql: createDatabasePreviewSql.value, source: t("production.sourceSidebar"), execute: () => executeAuthorizationPlan(plan, (step) => api.executeMulti(node.connectionId!, step.database, step.sql, undefined, undefined, { maxRows: 1000, continueOnError: true })), }); if (!results) return; - createDatabaseAuthorizationResults.value = results; - const created = results.some((result) => result.step.id === "create-database" && result.status === "success"); - const status = authorizationPlanStatus(results); + const displayResults = results.map((result) => ({ + ...result, + message: result.message && result.step.operation === "createDatabase" ? appendCreateDatabaseErrorHint(config?.db_type, result.message, t) : result.message, + })); + createDatabaseAuthorizationResults.value = displayResults; + const created = displayResults.some((result) => result.step.id === "create-database" && result.status === "success"); + const status = authorizationPlanStatus(displayResults); if (created) { await connectionStore.ensureVisibleDatabase(node.connectionId, name); await connectionStore.loadDatabases(node.connectionId, { force: true }); } toast(t(status === "success" ? "contextMenu.createDatabaseSuccess" : status === "partial" ? "contextMenu.createDatabasePartial" : "contextMenu.createDatabaseFailed", { name }), status === "success" ? 3000 : 5000); } catch (error: any) { - toast(t("contextMenu.tableOperationFailed", { message: error?.message || String(error) }), 5000); + const message = appendCreateDatabaseErrorHint(connectionStore.getConfig(node.connectionId)?.db_type, error?.message || String(error), t); + toast(t("contextMenu.tableOperationFailed", { message }), 8000); } finally { createDatabaseAuthorizationApplying.value = false; } diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 68897c01e..70c9739f6 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1784,6 +1784,7 @@ export default { confirmDropDatabaseTitle: "Drop Database", confirmDropDatabaseMessage: 'Are you sure you want to drop database "{name}"? This will permanently delete the database and all its data.', createDatabaseSuccess: 'Database "{name}" created', + mysqlCreateDatabasePermissionHint: "The current MySQL account cannot create this database. An account named root does not necessarily have administrator privileges. Use an administrator account with GRANT OPTION to grant CREATE permission, then try again.", createDatabasePartial: 'Database "{name}" was created, but some grants failed', createDatabaseFailed: 'Database "{name}" could not be created', createDatabaseUsers: "Grant access to users", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index fd7fb8fea..ecf5b4f76 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1719,6 +1719,7 @@ export default withEnglishFallback({ confirmDropDatabaseTitle: "Eliminar base de datos", confirmDropDatabaseMessage: '¿Estás seguro de que deseas eliminar la base de datos "{name}"? Esto borrará permanentemente la base de datos y todos sus datos.', createDatabaseSuccess: 'Base de datos "{name}" creada', + mysqlCreateDatabasePermissionHint: "La cuenta MySQL actual no puede crear esta base de datos. Una cuenta llamada root no tiene necesariamente privilegios de administrador. Use una cuenta de administrador con GRANT OPTION para conceder el permiso CREATE y vuelva a intentarlo.", createDatabasePartial: 'La base de datos "{name}" se creó, pero algunas concesiones fallaron', createDatabaseFailed: 'No se pudo crear la base de datos "{name}"', createDatabaseUsers: "Conceder acceso a usuarios", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 230d640b4..e2d377e58 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -1717,6 +1717,7 @@ export default withEnglishFallback({ confirmDropDatabaseTitle: "Elimina Database", confirmDropDatabaseMessage: 'Sei sicuro di voler eliminare il database "{name}"? Questa operazione eliminerà permanentemente il database e tutti i suoi dati.', createDatabaseSuccess: 'Database "{name}" creato', + mysqlCreateDatabasePermissionHint: "L'account MySQL corrente non può creare questo database. Un account denominato root non dispone necessariamente dei privilegi di amministratore. Usa un account amministratore con GRANT OPTION per concedere il privilegio CREATE, quindi riprova.", createDatabasePartial: 'Il database "{name}" è stato creato, ma alcune concessioni non sono riuscite', createDatabaseFailed: 'Impossibile creare il database "{name}"', createDatabaseUsers: "Concedi accesso agli utenti", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 57a808da8..1b587dfcc 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -1714,6 +1714,7 @@ export default withEnglishFallback({ confirmDropDatabaseTitle: "データベースを削除", confirmDropDatabaseMessage: "本当にデータベース「{name}」をドロップ(削除)しますか?データベースとすべてのデータが永久に削除されます。", createDatabaseSuccess: "データベース「{name}」を作成しました", + mysqlCreateDatabasePermissionHint: "現在の MySQL アカウントには、このデータベースを作成する権限がありません。root という名前のアカウントでも管理者権限があるとは限りません。GRANT OPTION を持つ管理者アカウントで CREATE 権限を付与してから、もう一度お試しください。", createDatabasePartial: "データベース「{name}」は作成されましたが、一部のユーザー権限付与に失敗しました", createDatabaseFailed: "データベース「{name}」を作成できませんでした", createDatabaseUsers: "アクセスを付与するユーザー", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 068d8d189..b4b0c9de3 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -1719,6 +1719,7 @@ export default withEnglishFallback({ confirmDropDatabaseTitle: "Remover Banco de Dados", confirmDropDatabaseMessage: 'Tem certeza de que deseja remover o banco de dados "{name}"? Isso excluirá permanentemente o banco de dados e todos os seus dados.', createDatabaseSuccess: 'Banco de dados "{name}" criado', + mysqlCreateDatabasePermissionHint: "A conta MySQL atual não pode criar este banco de dados. Uma conta chamada root não possui necessariamente privilégios de administrador. Use uma conta de administrador com GRANT OPTION para conceder a permissão CREATE e tente novamente.", createDatabasePartial: 'O banco de dados "{name}" foi criado, mas algumas concessões falharam', createDatabaseFailed: 'Não foi possível criar o banco de dados "{name}"', createDatabaseUsers: "Conceder acesso aos usuários", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 462430706..e9a60d445 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1784,6 +1784,7 @@ export default withEnglishFallback({ confirmDropDatabaseTitle: "删除数据库", confirmDropDatabaseMessage: "确定要删除数据库「{name}」吗?这将永久删除该数据库及其所有数据。", createDatabaseSuccess: "数据库「{name}」已创建", + mysqlCreateDatabasePermissionHint: "当前 MySQL 账号没有创建此数据库的权限。账号名为 root 也不代表拥有管理员权限,请使用具备 GRANT OPTION 的管理员账号授予 CREATE 权限后重试。", createDatabasePartial: "数据库「{name}」已创建,但部分用户授权失败", createDatabaseFailed: "数据库「{name}」创建失败", createDatabaseUsers: "关联用户授权", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 0ae088124..85d017aaa 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1719,6 +1719,7 @@ export default withEnglishFallback({ confirmDropDatabaseTitle: "刪除資料庫", confirmDropDatabaseMessage: "確定要刪除資料庫「{name}」嗎?這將永久刪除該資料庫及其所有資料。", createDatabaseSuccess: "資料庫「{name}」已建立", + mysqlCreateDatabasePermissionHint: "目前的 MySQL 帳號沒有建立此資料庫的權限。帳號名稱為 root 也不代表擁有管理員權限,請使用具備 GRANT OPTION 的管理員帳號授予 CREATE 權限後再試。", createDatabasePartial: "資料庫「{name}」已建立,但部分使用者授權失敗", createDatabaseFailed: "資料庫「{name}」建立失敗", createDatabaseUsers: "關聯使用者授權", diff --git a/apps/desktop/src/lib/__tests__/database/createDatabaseErrorHints.spec.ts b/apps/desktop/src/lib/__tests__/database/createDatabaseErrorHints.spec.ts new file mode 100644 index 000000000..e589094ef --- /dev/null +++ b/apps/desktop/src/lib/__tests__/database/createDatabaseErrorHints.spec.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { appendCreateDatabaseErrorHint } from "@/lib/database/createDatabaseErrorHints"; + +const hint = "Use an account with GRANT OPTION to grant CREATE permission."; +const t = (key: string) => (key === "contextMenu.mysqlCreateDatabasePermissionHint" ? hint : key); + +describe("appendCreateDatabaseErrorHint", () => { + it("adds guidance for MySQL error 1044", () => { + const message = appendCreateDatabaseErrorHint("mysql", "ERROR 1044 (42000): Access denied for user 'root'@'%' to database 'pro'", t); + + expect(message).toContain("ERROR 1044"); + expect(message).toContain(hint); + }); + + it("recognizes access-denied messages when the driver omits the error code", () => { + const message = appendCreateDatabaseErrorHint("mysql", "Access denied for user 'app'@'%' to database 'pro'", t); + + expect(message).toContain(hint); + }); + + it("does not alter unrelated or non-MySQL errors", () => { + expect(appendCreateDatabaseErrorHint("mysql", "ERROR 1007: Can't create database; database exists", t)).toBe("ERROR 1007: Can't create database; database exists"); + expect(appendCreateDatabaseErrorHint("postgres", "ERROR 1044: permission denied", t)).toBe("ERROR 1044: permission denied"); + }); + + it("does not append the same hint twice", () => { + const original = `ERROR 1044: Access denied\n\n${hint}`; + + expect(appendCreateDatabaseErrorHint("mysql", original, t)).toBe(original); + }); +}); diff --git a/apps/desktop/src/lib/database/createDatabaseErrorHints.ts b/apps/desktop/src/lib/database/createDatabaseErrorHints.ts new file mode 100644 index 000000000..5e4ab8d5b --- /dev/null +++ b/apps/desktop/src/lib/database/createDatabaseErrorHints.ts @@ -0,0 +1,13 @@ +import type { DatabaseType } from "@/types/database"; + +type Translate = (key: string) => string; + +function isMysqlCreateDatabaseAccessDenied(message: string): boolean { + return /\b1044\b/i.test(message) || /access denied for user[\s\S]*to database/i.test(message); +} + +export function appendCreateDatabaseErrorHint(databaseType: DatabaseType | undefined, message: string, t: Translate): string { + if (databaseType !== "mysql" || !isMysqlCreateDatabaseAccessDenied(message)) return message; + const hint = t("contextMenu.mysqlCreateDatabasePermissionHint"); + return message.includes(hint) ? message : `${message}\n\n${hint}`; +}