feat(mysql): add routine and trigger creation templates
This commit is contained in:
parent
bec6514fee
commit
71ce46e5fd
|
|
@ -66,6 +66,7 @@ import { resolveDefaultDatabase } from "@/lib/database/defaultDatabase";
|
|||
import { canTreeNodePin, canTreeNodeShowExpander } from "@/lib/sidebar/sidebarTreeItemLayout";
|
||||
import { objectTypesForGroupNode } from "@/lib/table/tableTree";
|
||||
import { loadSidebarObjectGroup } from "@/lib/sidebar/sidebarObjectGroupRouting";
|
||||
import { mysqlObjectTemplateForGroup } from "@/lib/sidebar/mysqlObjectTemplates";
|
||||
import { buildTableDeleteTemplate, buildTableInsertTemplate, buildTableSelectTemplate, buildTableUpdateTemplate } from "@/lib/table/tableSqlTemplates";
|
||||
import { driverStoreFocusForInstallError } from "@/lib/connection/agentDriverInstallHint";
|
||||
import {
|
||||
|
|
@ -2865,6 +2866,16 @@ function createView() {
|
|||
});
|
||||
}
|
||||
|
||||
function createMysqlObjectTemplate() {
|
||||
const node = activeNode.value;
|
||||
if (!node.connectionId || !node.database) return;
|
||||
const template = mysqlObjectTemplateForGroup(connectionStore.getConfig(node.connectionId), node);
|
||||
if (!template) return;
|
||||
connectionStore.activeConnectionId = node.connectionId;
|
||||
const tabId = queryStore.createTab(node.connectionId, node.database, t(template.titleKey), "query", node.schema);
|
||||
queryStore.updateSql(tabId, template.sql);
|
||||
}
|
||||
|
||||
const canExpand = computed(() =>
|
||||
canTreeNodeShowExpander({
|
||||
type: activeNode.value.type,
|
||||
|
|
@ -4024,7 +4035,8 @@ function buildObjectGroupSidebarMenu(context: SidebarMenuFactoryContext): boolea
|
|||
const { node, items } = context;
|
||||
// 9. Group Labels (group-columns, group-tables, etc.)
|
||||
if (isGroupLabel(node)) {
|
||||
const hasGroupCreateAction = (node.type === "group-tables" && canCreateTable.value) || (node.type === "group-views" && !!node.connectionId && !!node.database);
|
||||
const mysqlObjectTemplate = node.connectionId ? mysqlObjectTemplateForGroup(connectionStore.getConfig(node.connectionId), node) : null;
|
||||
const hasGroupCreateAction = (node.type === "group-tables" && canCreateTable.value) || (node.type === "group-views" && !!node.connectionId && !!node.database) || !!mysqlObjectTemplate;
|
||||
const canLoadAllObjectGroup = node.type === "group-tables" || node.type === "group-views" || node.type === "group-materialized-views";
|
||||
if (node.type === "group-tables" && canCreateTable.value) {
|
||||
items.push({ label: t("contextMenu.createTable"), action: createTable, icon: Plus });
|
||||
|
|
@ -4038,6 +4050,9 @@ function buildObjectGroupSidebarMenu(context: SidebarMenuFactoryContext): boolea
|
|||
if (node.type === "group-views" && node.connectionId && node.database) {
|
||||
items.push({ label: t("contextMenu.createView"), action: createView, icon: Plus });
|
||||
}
|
||||
if (mysqlObjectTemplate) {
|
||||
items.push({ label: t(mysqlObjectTemplate.titleKey), action: createMysqlObjectTemplate, icon: Plus });
|
||||
}
|
||||
if (hasGroupCreateAction) {
|
||||
items.push({ label: "", separator: true });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1644,6 +1644,9 @@ export default {
|
|||
exportStructureCopied: "Structure copied",
|
||||
createTable: "Create Table",
|
||||
createView: "Create View",
|
||||
createProcedure: "Create Procedure",
|
||||
createFunction: "Create Function",
|
||||
createTrigger: "Create Trigger",
|
||||
dropTable: "Drop Table",
|
||||
emptyTable: "Empty Table",
|
||||
truncateTable: "Truncate Table",
|
||||
|
|
|
|||
|
|
@ -1761,6 +1761,9 @@ export default withEnglishFallback({
|
|||
dropExtension: "Eliminar extensión",
|
||||
processList: "Lista de procesos",
|
||||
serverDashboard: "Panel de control del servidor",
|
||||
createProcedure: "Nuevo procedimiento almacenado",
|
||||
createFunction: "Nueva función",
|
||||
createTrigger: "Nuevo disparador",
|
||||
},
|
||||
visibleDatabases: {
|
||||
title: "Bases de datos visibles",
|
||||
|
|
|
|||
|
|
@ -1759,6 +1759,9 @@ export default withEnglishFallback({
|
|||
dropExtension: "Elimina estensione",
|
||||
processList: "Elenco processi",
|
||||
serverDashboard: "Dashboard del server",
|
||||
createProcedure: "Nuovo stored procedure",
|
||||
createFunction: "Nuova funzione",
|
||||
createTrigger: "Nuovo trigger",
|
||||
},
|
||||
visibleDatabases: {
|
||||
title: "Database Visibili",
|
||||
|
|
|
|||
|
|
@ -1760,6 +1760,9 @@ export default withEnglishFallback({
|
|||
dropExtension: "拡張機能を削除",
|
||||
processList: "プロセス一覧",
|
||||
serverDashboard: "サーバーダッシュボード",
|
||||
createProcedure: "新規ストアドプロシージャ",
|
||||
createFunction: "新規関数",
|
||||
createTrigger: "新規トリガー",
|
||||
},
|
||||
visibleDatabases: {
|
||||
title: "表示するデータベース",
|
||||
|
|
|
|||
|
|
@ -1761,6 +1761,9 @@ export default withEnglishFallback({
|
|||
dropExtension: "Remover extensão",
|
||||
processList: "Lista de Processos",
|
||||
serverDashboard: "Painel do Servidor",
|
||||
createProcedure: "Novo procedimento armazenado",
|
||||
createFunction: "Nova função",
|
||||
createTrigger: "Novo gatilho",
|
||||
},
|
||||
visibleDatabases: {
|
||||
title: "Bancos de dados visíveis",
|
||||
|
|
|
|||
|
|
@ -1644,6 +1644,9 @@ export default withEnglishFallback({
|
|||
exportStructureCopied: "表结构已复制",
|
||||
createTable: "新建表",
|
||||
createView: "新建视图",
|
||||
createProcedure: "新建存储过程",
|
||||
createFunction: "新建函数",
|
||||
createTrigger: "新建触发器",
|
||||
dropTable: "删除表",
|
||||
emptyTable: "清空数据",
|
||||
truncateTable: "截断表",
|
||||
|
|
|
|||
|
|
@ -1761,6 +1761,9 @@ export default withEnglishFallback({
|
|||
dropExtension: "刪除擴展",
|
||||
processList: "處理程序清單",
|
||||
serverDashboard: "伺服器儀表板",
|
||||
createProcedure: "新增儲存程序",
|
||||
createFunction: "新增函數",
|
||||
createTrigger: "新增觸發器",
|
||||
},
|
||||
visibleDatabases: {
|
||||
title: "顯示資料庫",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
import type { ConnectionConfig, TreeNode, TreeNodeType } from "@/types/database";
|
||||
|
||||
export type MysqlObjectTemplateKind = "procedure" | "function" | "trigger";
|
||||
|
||||
export interface MysqlObjectTemplate {
|
||||
kind: MysqlObjectTemplateKind;
|
||||
titleKey: "contextMenu.createProcedure" | "contextMenu.createFunction" | "contextMenu.createTrigger";
|
||||
sql: string;
|
||||
}
|
||||
|
||||
type MysqlTemplateConnection = Pick<ConnectionConfig, "db_type" | "driver_profile">;
|
||||
type MysqlTemplateNode = Pick<TreeNode, "type" | "database" | "tableName">;
|
||||
|
||||
const templateKindByGroup: Partial<Record<TreeNodeType, MysqlObjectTemplateKind>> = {
|
||||
"group-procedures": "procedure",
|
||||
"group-functions": "function",
|
||||
"group-triggers": "trigger",
|
||||
};
|
||||
|
||||
export function supportsMysqlObjectTemplates(connection?: MysqlTemplateConnection): boolean {
|
||||
if (connection?.db_type !== "mysql") return false;
|
||||
const profile = connection.driver_profile?.trim().toLowerCase();
|
||||
return !profile || profile === "mysql" || profile === "custom_mysql";
|
||||
}
|
||||
|
||||
export function mysqlObjectTemplateForGroup(connection: MysqlTemplateConnection | undefined, node: MysqlTemplateNode): MysqlObjectTemplate | null {
|
||||
if (!supportsMysqlObjectTemplates(connection) || !node.database) return null;
|
||||
const kind = templateKindByGroup[node.type];
|
||||
if (!kind) return null;
|
||||
return buildMysqlObjectTemplate(kind, node.database, node.tableName);
|
||||
}
|
||||
|
||||
export function buildMysqlObjectTemplate(kind: MysqlObjectTemplateKind, database: string, tableName?: string): MysqlObjectTemplate {
|
||||
const qualifiedDatabase = quoteMysqlIdentifier(database);
|
||||
|
||||
if (kind === "procedure") {
|
||||
const name = "new_procedure";
|
||||
return {
|
||||
kind,
|
||||
titleKey: "contextMenu.createProcedure",
|
||||
sql: `DELIMITER $$
|
||||
|
||||
CREATE PROCEDURE ${qualifiedDatabase}.${quoteMysqlIdentifier(name)}()
|
||||
BEGIN
|
||||
SELECT 1;
|
||||
END$$
|
||||
|
||||
DELIMITER ;`,
|
||||
};
|
||||
}
|
||||
|
||||
if (kind === "function") {
|
||||
const name = "new_function";
|
||||
return {
|
||||
kind,
|
||||
titleKey: "contextMenu.createFunction",
|
||||
sql: `DELIMITER $$
|
||||
|
||||
CREATE FUNCTION ${qualifiedDatabase}.${quoteMysqlIdentifier(name)}()
|
||||
RETURNS INT
|
||||
DETERMINISTIC
|
||||
BEGIN
|
||||
RETURN 0;
|
||||
END$$
|
||||
|
||||
DELIMITER ;`,
|
||||
};
|
||||
}
|
||||
|
||||
const name = "new_trigger";
|
||||
return {
|
||||
kind,
|
||||
titleKey: "contextMenu.createTrigger",
|
||||
sql: `DELIMITER $$
|
||||
|
||||
CREATE TRIGGER ${qualifiedDatabase}.${quoteMysqlIdentifier(name)}
|
||||
BEFORE INSERT ON ${qualifiedDatabase}.${quoteMysqlIdentifier(tableName || "table_name")}
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
SET @dbx_trigger_placeholder = 1;
|
||||
END$$
|
||||
|
||||
DELIMITER ;`,
|
||||
};
|
||||
}
|
||||
|
||||
function quoteMysqlIdentifier(identifier: string): string {
|
||||
return `\`${identifier.replaceAll("`", "``")}\``;
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { test } from "vitest";
|
||||
import { buildMysqlObjectTemplate, mysqlObjectTemplateForGroup, supportsMysqlObjectTemplates } from "../../apps/desktop/src/lib/sidebar/mysqlObjectTemplates.ts";
|
||||
import { executableStatementRanges } from "../../apps/desktop/src/lib/sql/sqlStatementRanges.ts";
|
||||
import type { ConnectionConfig, TreeNode } from "../../apps/desktop/src/types/database.ts";
|
||||
|
||||
function connection(dbType: ConnectionConfig["db_type"], driverProfile?: string): ConnectionConfig {
|
||||
return {
|
||||
id: "conn-1",
|
||||
name: "Test",
|
||||
db_type: dbType,
|
||||
driver_profile: driverProfile,
|
||||
host: "127.0.0.1",
|
||||
port: 3306,
|
||||
username: "root",
|
||||
password: "",
|
||||
};
|
||||
}
|
||||
|
||||
function group(type: TreeNode["type"], overrides: Partial<TreeNode> = {}): TreeNode {
|
||||
return {
|
||||
id: `conn-1:app:${type}`,
|
||||
label: type,
|
||||
type,
|
||||
connectionId: "conn-1",
|
||||
database: "app",
|
||||
children: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("MySQL object templates are limited to native and custom MySQL profiles", () => {
|
||||
assert.equal(supportsMysqlObjectTemplates(connection("mysql")), true);
|
||||
assert.equal(supportsMysqlObjectTemplates(connection("mysql", "mysql")), true);
|
||||
assert.equal(supportsMysqlObjectTemplates(connection("mysql", "CUSTOM_MYSQL")), true);
|
||||
|
||||
for (const profile of ["doris", "selectdb", "starrocks", "tidb", "oceanbase", "mariadb"]) {
|
||||
assert.equal(supportsMysqlObjectTemplates(connection("mysql", profile)), false, profile);
|
||||
}
|
||||
assert.equal(supportsMysqlObjectTemplates(connection("starrocks", "starrocks")), false);
|
||||
assert.equal(supportsMysqlObjectTemplates(connection("jdbc", "mysql")), false);
|
||||
});
|
||||
|
||||
test("only supported MySQL object groups expose creation templates", () => {
|
||||
const mysql = connection("mysql", "mysql");
|
||||
assert.equal(mysqlObjectTemplateForGroup(mysql, group("group-procedures"))?.kind, "procedure");
|
||||
assert.equal(mysqlObjectTemplateForGroup(mysql, group("group-functions"))?.kind, "function");
|
||||
assert.equal(mysqlObjectTemplateForGroup(mysql, group("group-triggers"))?.kind, "trigger");
|
||||
assert.equal(mysqlObjectTemplateForGroup(mysql, group("group-views")), null);
|
||||
assert.equal(mysqlObjectTemplateForGroup(mysql, group("group-functions", { database: "" })), null);
|
||||
});
|
||||
|
||||
test("procedure and function templates are executable and database-qualified", () => {
|
||||
assert.equal(buildMysqlObjectTemplate("procedure", "db`x").sql, "DELIMITER $$\n\nCREATE PROCEDURE `db``x`.`new_procedure`()\nBEGIN\n SELECT 1;\nEND$$\n\nDELIMITER ;");
|
||||
assert.equal(buildMysqlObjectTemplate("function", "app").sql, "DELIMITER $$\n\nCREATE FUNCTION `app`.`new_function`()\nRETURNS INT\nDETERMINISTIC\nBEGIN\n RETURN 0;\nEND$$\n\nDELIMITER ;");
|
||||
});
|
||||
|
||||
test("DBX parses every template as one executable MySQL statement", () => {
|
||||
for (const kind of ["procedure", "function", "trigger"] as const) {
|
||||
const ranges = executableStatementRanges(buildMysqlObjectTemplate(kind, "app", "orders").sql, "mysql");
|
||||
assert.equal(ranges.length, 1, kind);
|
||||
assert.match(ranges[0].sql, new RegExp(`^CREATE ${kind.toUpperCase()}`));
|
||||
assert.doesNotMatch(ranges[0].sql, /DELIMITER/i);
|
||||
}
|
||||
});
|
||||
|
||||
test("trigger templates use table context when available and otherwise keep an editable placeholder", () => {
|
||||
assert.match(buildMysqlObjectTemplate("trigger", "app", "order`items").sql, /BEFORE INSERT ON `app`\.`order``items`/);
|
||||
assert.match(buildMysqlObjectTemplate("trigger", "app").sql, /BEFORE INSERT ON `app`\.`table_name`/);
|
||||
|
||||
const template = mysqlObjectTemplateForGroup(connection("mysql", "mysql"), group("group-triggers", { tableName: "orders" }));
|
||||
assert.equal(template?.kind, "trigger");
|
||||
assert.match(template?.sql || "", /BEFORE INSERT ON `app`\.`orders`/);
|
||||
});
|
||||
Loading…
Reference in New Issue