feat(xugu): add programmable object compile actions
* feat(xugu): add programmable object compile actions * chore(i18n): autofill new translations --------- Co-authored-by: mapan <pan.anarkh1994@gmail.com> Co-authored-by: dbx-i18n-bot <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
c3bb16f93e
commit
352403d79e
|
|
@ -44,6 +44,7 @@ import {
|
|||
Trash2,
|
||||
WrapText,
|
||||
X,
|
||||
Wrench,
|
||||
} from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import i18n from "@/i18n";
|
||||
|
|
@ -94,6 +95,7 @@ import QueryEditor from "@/components/editor/QueryEditor.vue";
|
|||
import { sqlFormatDialectForDbType, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
|
||||
import { isCancelSearchShortcut } from "@/lib/editor/keyboardShortcuts";
|
||||
import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
|
||||
import { buildXuguCompileSql } from "@/lib/database/xuguCompileSql";
|
||||
import { formatShortcut } from "@/lib/editor/shortcutRegistry";
|
||||
import { batchTableEmptyFeedback, buildBatchTableEmptyPlan, runBatchTableEmpty, type BatchTableEmptyPlanItem } from "@/lib/sidebar/batchTableEmpty";
|
||||
import { runBatchTableDrop } from "@/lib/table/batchTableDrop";
|
||||
|
|
@ -2199,6 +2201,21 @@ async function executeObjectBrowserSqlWithProductionGuard<T>(sql: string, execut
|
|||
});
|
||||
}
|
||||
|
||||
async function compileXuguObject(row: ObjectBrowserRow) {
|
||||
if (effectiveDatabaseType.value !== "xugu") return;
|
||||
const sql = buildXuguCompileSql({ objectType: row.type, schema: row.schema || selectedSchema.value, name: row.name });
|
||||
if (!sql) return;
|
||||
try {
|
||||
const executed = await executeObjectBrowserSqlWithProductionGuard(sql, () => api.executeQuery(props.connection.id, props.database, sql, row.schema || selectedSchema.value));
|
||||
if (!executed) return;
|
||||
toast(t("contextMenu.compileObjectSuccess", { name: row.name }));
|
||||
await reload();
|
||||
await connectionStore.refreshObjectListTreeNode(props.connection.id, props.database, row.schema || selectedSchema.value);
|
||||
} catch (e: any) {
|
||||
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshTruncatePreviewSql(row: ObjectBrowserRow) {
|
||||
truncatePreviewSql.value = "";
|
||||
truncatePreviewSql.value = await buildTruncateTableSql(tableAdminSqlOptions(row, { cascade: canTruncateTargetCascade.value && truncateTableCascade.value })).catch(() => "");
|
||||
|
|
@ -2704,6 +2721,7 @@ function getViewMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
|||
function getProcFuncMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
return [
|
||||
...(item.type === "PROCEDURE" ? [{ label: t("contextMenu.executeProcedure"), action: () => openProcedureExecution(item), icon: Play }] : []),
|
||||
...(effectiveDatabaseType.value === "xugu" && buildXuguCompileSql({ objectType: item.type, schema: item.schema || selectedSchema.value, name: item.name }) ? [{ label: t("contextMenu.compileObject"), action: () => compileXuguObject(item), icon: Wrench }] : []),
|
||||
{ label: t("contextMenu.viewSource"), action: () => openSource(item), icon: Code2 },
|
||||
...(canRename(item) ? [{ label: t("contextMenu.renameObject"), action: () => requestRename(item), icon: Pencil }] : []),
|
||||
{ label: "", separator: true },
|
||||
|
|
@ -2720,6 +2738,7 @@ function getProcFuncMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
|||
|
||||
function getPackageMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
return [
|
||||
...(effectiveDatabaseType.value === "xugu" && buildXuguCompileSql({ objectType: item.type, schema: item.schema || selectedSchema.value, name: item.name }) ? [{ label: t("contextMenu.compileObject"), action: () => compileXuguObject(item), icon: Wrench }] : []),
|
||||
{ label: t("contextMenu.viewSource"), action: () => openSource(item), icon: Code2 },
|
||||
{ label: "", separator: true },
|
||||
{ label: t("contextMenu.copyName"), action: () => copyName(item), icon: Copy },
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import {
|
|||
Plus,
|
||||
ScrollText,
|
||||
Code2,
|
||||
Wrench,
|
||||
ListFilter,
|
||||
Clipboard,
|
||||
UsersRound,
|
||||
|
|
@ -154,6 +155,7 @@ import { isSqlServerLinkedNode } from "@/lib/database/sqlServerLinkedServers";
|
|||
import { flattenTree } from "@/composables/useFlatTree";
|
||||
import { createDatabaseCollationOptionsForCharset, nextCreateDatabaseCollation, normalizeCreateDatabaseCharset, parseCreateDatabaseCharsetMetadata } from "@/lib/database/createDatabaseCharsetOptions";
|
||||
import { executeWithProductionContextGuard, executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
|
||||
import { buildXuguCompileSql } from "@/lib/database/xuguCompileSql";
|
||||
import type { SidebarDataOpenRequest } from "@/lib/sidebar/sidebarDataOpenCoordinator";
|
||||
import { createSidebarActionTarget, findSidebarActionTarget, releaseRemovedSidebarActionTarget, type SidebarActionTarget } from "@/lib/sidebar/sidebarActionTarget";
|
||||
import { createSidebarMenuContext, normalizeSidebarMenuDescriptors } from "@/lib/sidebar/sidebarTreeMenuDescriptors";
|
||||
|
|
@ -1684,6 +1686,22 @@ function openProcedureExecution() {
|
|||
emit("open-procedure", node);
|
||||
}
|
||||
|
||||
async function compileXuguObject() {
|
||||
const node = activeNode.value;
|
||||
if (currentDatabaseType() !== "xugu" || !node.connectionId || !node.database) return;
|
||||
const sql = buildXuguCompileSql({ objectType: node.type, schema: node.schema, name: node.objectName || node.label });
|
||||
if (!sql) return;
|
||||
try {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
const executed = await executeTreeNodeSqlWithProductionGuard(node, sql, { database: node.database, schema: node.schema });
|
||||
if (!executed) return;
|
||||
toast(t("contextMenu.compileObjectSuccess", { name: node.label }), 3000);
|
||||
await connectionStore.refreshTreeNode(node);
|
||||
} catch (e: any) {
|
||||
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function requestDropObject() {
|
||||
void refreshDropObjectPreviewSql();
|
||||
showDropObjectConfirm.value = true;
|
||||
|
|
@ -4545,6 +4563,9 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
|||
|
||||
if (node.type === "index" || node.type === "fkey" || (node.type === "trigger" && !!node.tableName)) {
|
||||
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
||||
if (currentDatabaseType() === "xugu" && buildXuguCompileSql({ objectType: node.type, schema: node.schema, name: node.objectName || node.label })) {
|
||||
items.push({ label: t("contextMenu.compileObject"), action: compileXuguObject, icon: Wrench });
|
||||
}
|
||||
if (node.type === "index" && canOpenStructureEditor.value) {
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("contextMenu.editIndex"), action: openStructureEditor, icon: PencilRuler });
|
||||
|
|
@ -4576,6 +4597,9 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
|||
if (node.type === "procedure") {
|
||||
items.push({ label: t("contextMenu.executeProcedure"), action: openProcedureExecution, icon: Play });
|
||||
}
|
||||
if (currentDatabaseType() === "xugu" && buildXuguCompileSql({ objectType: node.type, schema: node.schema, name: node.objectName || node.label })) {
|
||||
items.push({ label: t("contextMenu.compileObject"), action: compileXuguObject, icon: Wrench });
|
||||
}
|
||||
items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 });
|
||||
if (canRenameObject.value) {
|
||||
items.push({
|
||||
|
|
@ -4606,6 +4630,9 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
|||
}
|
||||
|
||||
if (node.type === "trigger" || node.type === "package" || node.type === "package-body" || node.type === "type" || node.type === "type-body") {
|
||||
if (currentDatabaseType() === "xugu" && buildXuguCompileSql({ objectType: node.type, schema: node.schema, name: node.objectName || node.label })) {
|
||||
items.push({ label: t("contextMenu.compileObject"), action: compileXuguObject, icon: Wrench });
|
||||
}
|
||||
items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 });
|
||||
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
||||
items.push({ label: t("contextMenu.changeOpenMode"), action: () => emit("open-settings", "navigation"), icon: Settings2 });
|
||||
|
|
|
|||
|
|
@ -2326,6 +2326,8 @@ export default {
|
|||
batchTruncate: "Truncate selected ({count})",
|
||||
batchDropIndexes: "Drop Indexes ({count})",
|
||||
executeProcedure: "Execute Procedure",
|
||||
compileObject: "Compile",
|
||||
compileObjectSuccess: '"{name}" compiled successfully',
|
||||
confirmExecuteProcedureTitle: "Execute Procedure",
|
||||
confirmExecuteProcedureMessage: 'Execute procedure "{name}"? You can fill or adjust parameter values first.',
|
||||
loadingProcedureParameters: "Loading parameters...",
|
||||
|
|
|
|||
|
|
@ -2415,6 +2415,8 @@ export default withEnglishFallback({
|
|||
createTrigger: "Nuevo disparador",
|
||||
changeOpenMode: "Modificar modo de apertura",
|
||||
closeRightTabs: "Cerrar pestañas a la derecha",
|
||||
compileObject: "Compilar",
|
||||
compileObjectSuccess: "\"{name}\" compilado con éxito",
|
||||
},
|
||||
visibleDatabases: {
|
||||
title: "Bases de datos visibles",
|
||||
|
|
|
|||
|
|
@ -2413,6 +2413,8 @@ export default withEnglishFallback({
|
|||
createTrigger: "Nuovo trigger",
|
||||
changeOpenMode: "Modifica modalità apertura",
|
||||
closeRightTabs: "Chiudi le schede a destra",
|
||||
compileObject: "Compila",
|
||||
compileObjectSuccess: "\"{name}\" compilato con successo",
|
||||
},
|
||||
visibleDatabases: {
|
||||
title: "Database Visibili",
|
||||
|
|
|
|||
|
|
@ -2440,6 +2440,8 @@ export default withEnglishFallback({
|
|||
createTrigger: "新規トリガー",
|
||||
changeOpenMode: "開き方を変更",
|
||||
closeRightTabs: "右側のタブを閉じる",
|
||||
compileObject: "コンパイル",
|
||||
compileObjectSuccess: "\"{name}\" のコンパイルに成功しました",
|
||||
},
|
||||
visibleDatabases: {
|
||||
title: "表示するデータベース",
|
||||
|
|
|
|||
|
|
@ -2415,6 +2415,8 @@ export default withEnglishFallback({
|
|||
createTrigger: "Novo gatilho",
|
||||
changeOpenMode: "Alterar modo de abertura",
|
||||
closeRightTabs: "Fechar abas à direita",
|
||||
compileObject: "Compilar",
|
||||
compileObjectSuccess: "\"{name}\" compilado com sucesso",
|
||||
},
|
||||
visibleDatabases: {
|
||||
title: "Bancos de dados visíveis",
|
||||
|
|
|
|||
|
|
@ -2327,6 +2327,8 @@ export default withEnglishFallback({
|
|||
batchTruncate: "截断所选({count})",
|
||||
batchDropIndexes: "删除索引({count})",
|
||||
executeProcedure: "执行过程",
|
||||
compileObject: "编译",
|
||||
compileObjectSuccess: '"{name}" 编译成功',
|
||||
confirmExecuteProcedureTitle: "执行存储过程",
|
||||
confirmExecuteProcedureMessage: "确认执行存储过程「{name}」?可先补充或调整参数值。",
|
||||
loadingProcedureParameters: "正在读取参数...",
|
||||
|
|
|
|||
|
|
@ -2414,6 +2414,8 @@ export default withEnglishFallback({
|
|||
createTrigger: "新增觸發器",
|
||||
changeOpenMode: "修改開啟方式",
|
||||
closeRightTabs: "關閉右側標籤頁",
|
||||
compileObject: "編譯",
|
||||
compileObjectSuccess: "\"{name}\" 編譯成功",
|
||||
},
|
||||
visibleDatabases: {
|
||||
title: "顯示資料庫",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { buildXuguCompileSql, xuguCompileKeyword } from "@/lib/database/xuguCompileSql";
|
||||
|
||||
describe("xuguCompileSql", () => {
|
||||
it.each([
|
||||
["procedure", "PROCEDURE"],
|
||||
["function", "FUNCTION"],
|
||||
["trigger", "TRIGGER"],
|
||||
["package", "PACKAGE"],
|
||||
["package-body", "PACKAGE"],
|
||||
["type", "TYPE"],
|
||||
["type-body", "TYPE"],
|
||||
])("maps %s to %s", (objectType, keyword) => {
|
||||
expect(xuguCompileKeyword(objectType)).toBe(keyword);
|
||||
});
|
||||
|
||||
it("quotes schema and object names while preserving mixed case", () => {
|
||||
expect(buildXuguCompileSql({ objectType: "procedure", schema: "AppSchema", name: "spCreateOrder" })).toBe('ALTER PROCEDURE "AppSchema"."spCreateOrder" RECOMPILE;');
|
||||
});
|
||||
|
||||
it("accepts object-browser uppercase kinds", () => {
|
||||
expect(buildXuguCompileSql({ objectType: "FUNCTION", name: "f_get_count" })).toBe('ALTER FUNCTION "f_get_count" RECOMPILE;');
|
||||
});
|
||||
|
||||
it("escapes embedded double quotes", () => {
|
||||
expect(buildXuguCompileSql({ objectType: "trigger", schema: 'A"Schema', name: 'T"1' })).toBe('ALTER TRIGGER "A""Schema"."T""1" RECOMPILE;');
|
||||
});
|
||||
|
||||
it("returns null for unsupported object kinds or blank names", () => {
|
||||
expect(buildXuguCompileSql({ objectType: "sequence", schema: "SYSDBA", name: "SEQ_1" })).toBeNull();
|
||||
expect(buildXuguCompileSql({ objectType: "procedure", schema: "SYSDBA", name: " " })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Xugu object kinds that support an explicit recompilation statement.
|
||||
* Keep this list intentionally narrow: the caller must still gate the action
|
||||
* to Xugu connections so other database dialects are not affected.
|
||||
*/
|
||||
const XUGU_COMPILE_KEYWORDS: Record<string, string> = {
|
||||
procedure: "PROCEDURE",
|
||||
function: "FUNCTION",
|
||||
trigger: "TRIGGER",
|
||||
package: "PACKAGE",
|
||||
"package-body": "PACKAGE",
|
||||
type: "TYPE",
|
||||
"type-body": "TYPE",
|
||||
PROCEDURE: "PROCEDURE",
|
||||
FUNCTION: "FUNCTION",
|
||||
TRIGGER: "TRIGGER",
|
||||
PACKAGE: "PACKAGE",
|
||||
PACKAGE_BODY: "PACKAGE",
|
||||
TYPE: "TYPE",
|
||||
TYPE_BODY: "TYPE",
|
||||
};
|
||||
|
||||
export interface XuguCompileSqlInput {
|
||||
objectType: string;
|
||||
name: string;
|
||||
schema?: string;
|
||||
}
|
||||
|
||||
export function xuguCompileKeyword(objectType: string): string | null {
|
||||
return XUGU_COMPILE_KEYWORDS[objectType] ?? null;
|
||||
}
|
||||
|
||||
export function buildXuguCompileSql(input: XuguCompileSqlInput): string | null {
|
||||
const keyword = xuguCompileKeyword(input.objectType);
|
||||
const name = input.name.trim();
|
||||
if (!keyword || !name) return null;
|
||||
const qualifiedName = input.schema?.trim() ? `${quoteXuguIdentifier(input.schema.trim())}.${quoteXuguIdentifier(name)}` : quoteXuguIdentifier(name);
|
||||
return `ALTER ${keyword} ${qualifiedName} RECOMPILE;`;
|
||||
}
|
||||
|
||||
function quoteXuguIdentifier(name: string): string {
|
||||
return `"${name.replaceAll('"', '""')}"`;
|
||||
}
|
||||
Loading…
Reference in New Issue