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:
Elias 2026-08-07 03:30:52 +08:00 committed by GitHub
parent c3bb16f93e
commit 352403d79e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 136 additions and 0 deletions

View File

@ -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 },

View File

@ -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 });

View File

@ -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...",

View File

@ -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",

View File

@ -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",

View File

@ -2440,6 +2440,8 @@ export default withEnglishFallback({
createTrigger: "新規トリガー",
changeOpenMode: "開き方を変更",
closeRightTabs: "右側のタブを閉じる",
compileObject: "コンパイル",
compileObjectSuccess: "\"{name}\" のコンパイルに成功しました",
},
visibleDatabases: {
title: "表示するデータベース",

View File

@ -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",

View File

@ -2327,6 +2327,8 @@ export default withEnglishFallback({
batchTruncate: "截断所选({count}",
batchDropIndexes: "删除索引({count}",
executeProcedure: "执行过程",
compileObject: "编译",
compileObjectSuccess: '"{name}" 编译成功',
confirmExecuteProcedureTitle: "执行存储过程",
confirmExecuteProcedureMessage: "确认执行存储过程「{name}」?可先补充或调整参数值。",
loadingProcedureParameters: "正在读取参数...",

View File

@ -2414,6 +2414,8 @@ export default withEnglishFallback({
createTrigger: "新增觸發器",
changeOpenMode: "修改開啟方式",
closeRightTabs: "關閉右側標籤頁",
compileObject: "編譯",
compileObjectSuccess: "\"{name}\" 編譯成功",
},
visibleDatabases: {
title: "顯示資料庫",

View File

@ -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();
});
});

View File

@ -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('"', '""')}"`;
}