feat(dameng): show view validity and support recompilation
This commit is contained in:
1 parent
e4bda693fb
commit
6b81a98453
71 files changed
+1040
-38
No files matched your search
@@ -124,6 +124,7 @@ import { isCancelSearchShortcut } from "@/lib/editor/keyboardShortcuts";
|
||||
import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
|
||||
import { connectionIsEffectivelyReadOnly } from "@/lib/database/readOnlyWriteAccess";
|
||||
import { buildXuguCompileSql } from "@/lib/database/xuguCompileSql";
|
||||
import { buildDamengCompileViewSql } from "@/lib/database/damengCompileSql";
|
||||
import { formatShortcut } from "@/lib/editor/shortcutRegistry";
|
||||
import { batchTableEmptyFeedback, buildBatchTableEmptyPlan, runBatchTableEmpty, type BatchTableEmptyPlanItem } from "@/lib/sidebar/batchTableEmpty";
|
||||
import { runBatchTableDrop } from "@/lib/table/batchTableDrop";
|
||||
@@ -224,6 +225,9 @@ const sourceError = ref("");
|
||||
const sourceRow = ref<ObjectBrowserRow | null>(null);
|
||||
const sourceEditing = ref(false);
|
||||
const sourceCanEdit = ref(true);
|
||||
const showCompileErrorDialog = ref(false);
|
||||
const compileErrorTitle = ref("");
|
||||
const compileErrorMessage = ref("");
|
||||
// --- Right-side panel state ---
|
||||
// Unified panel: either "table-info" (for tables) or "source" (for views/procedures/etc.)
|
||||
const sidePanelRow = ref<ObjectBrowserRow | null>(null);
|
||||
@@ -2570,6 +2574,24 @@ async function compileXuguObject(row: ObjectBrowserRow) {
|
||||
}
|
||||
}
|
||||
|
||||
async function compileDamengView(row: ObjectBrowserRow) {
|
||||
if (effectiveDatabaseType.value !== "dameng" || row.type !== "VIEW") return;
|
||||
const schema = row.schema || selectedSchema.value;
|
||||
const sql = buildDamengCompileViewSql({ schema, name: row.name });
|
||||
if (!sql) return;
|
||||
try {
|
||||
const executed = await executeObjectBrowserSqlWithProductionGuard(sql, () => api.executeQuery(props.connection.id, props.database, sql, schema));
|
||||
if (!executed) return;
|
||||
toast(t("contextMenu.compileObjectSuccess", { name: row.name }));
|
||||
await reload();
|
||||
await connectionStore.refreshObjectListTreeNode(props.connection.id, props.database, schema);
|
||||
} catch (e: any) {
|
||||
compileErrorTitle.value = t("contextMenu.compileObjectFailedTitle");
|
||||
compileErrorMessage.value = t("contextMenu.compileObjectFailedMessage", { name: row.name, message: e?.message || String(e) });
|
||||
showCompileErrorDialog.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshTruncatePreviewSql(row: ObjectBrowserRow) {
|
||||
truncatePreviewSql.value = "";
|
||||
truncatePreviewSql.value = await buildTruncateTableSql(tableAdminSqlOptions(row, { cascade: canTruncateTargetCascade.value && truncateTableCascade.value })).catch(() => "");
|
||||
@@ -3244,6 +3266,7 @@ function getViewMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
{ label: t("contextMenu.viewData"), action: () => openViewData(item), icon: Table2 },
|
||||
{ label: t("contextMenu.editView"), action: () => openSource(item), icon: PencilLine },
|
||||
{ label: t("contextMenu.viewSource"), action: () => openSource(item), icon: Code2 },
|
||||
...(effectiveDatabaseType.value === "dameng" && item.type === "VIEW" && buildDamengCompileViewSql({ schema: item.schema || selectedSchema.value, name: item.name }) ? [{ label: t("contextMenu.compileObject"), action: () => compileDamengView(item), icon: Wrench }] : []),
|
||||
{
|
||||
label: t("contextMenu.viewDdl"),
|
||||
action: () => openTableInfo(item, "ddl"),
|
||||
@@ -3633,7 +3656,12 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
{{ t("objects.partitions", { count: item.partitionCount }) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="truncate text-xs text-muted-foreground">{{ typeLabel(item) }}</div>
|
||||
<div class="flex min-w-0 items-center gap-1.5 truncate text-xs text-muted-foreground">
|
||||
<span class="truncate">{{ typeLabel(item) }}</span>
|
||||
<span v-if="item.type === 'VIEW' && item.valid != null" class="shrink-0 rounded border px-1 py-px text-[10px] font-medium" :class="item.valid ? 'border-emerald-500/30 text-emerald-600' : 'border-destructive/30 text-destructive'">
|
||||
{{ t(item.valid ? "objects.validStatus" : "objects.invalidStatus") }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="showObjectRowStats" class="truncate text-xs tabular-nums text-muted-foreground" :title="item.estimatedRows == null ? '' : formatObjectBrowserCount(item.estimatedRows)">
|
||||
{{ formatObjectBrowserCount(item.estimatedRows) }}
|
||||
</div>
|
||||
@@ -3679,6 +3707,9 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
<span class="w-full truncate text-sm font-medium leading-tight text-foreground">{{ item.displayName }}</span>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="text-xs text-muted-foreground">{{ typeLabel(item) }}</span>
|
||||
<span v-if="item.type === 'VIEW' && item.valid != null" class="rounded border px-1 py-px text-[10px] font-medium" :class="item.valid ? 'border-emerald-500/30 text-emerald-600' : 'border-destructive/30 text-destructive'">
|
||||
{{ t(item.valid ? "objects.validStatus" : "objects.invalidStatus") }}
|
||||
</span>
|
||||
<span v-if="showObjectRowStats && item.estimatedRows != null && item.estimatedRows > 0" class="object-browser-stat-badge object-browser-stat-badge-rows rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium tabular-nums text-primary">{{
|
||||
formatObjectBrowserCount(item.estimatedRows)
|
||||
}}</span>
|
||||
@@ -4058,6 +4089,18 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showCompileErrorDialog">
|
||||
<DialogContent class="sm:max-w-[560px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ compileErrorTitle }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<pre class="max-h-72 overflow-auto whitespace-pre-wrap break-words rounded bg-destructive/5 p-3 text-sm text-destructive">{{ compileErrorMessage }}</pre>
|
||||
<DialogFooter>
|
||||
<Button @click="showCompileErrorDialog = false">{{ t("common.close") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<DangerConfirmDialog
|
||||
v-model:open="showVacuumConfirm"
|
||||
:title="t('contextMenu.vacuumTableTitle')"
|
||||
|
||||
@@ -131,6 +131,9 @@ const {
|
||||
createSchemaName,
|
||||
confirmCreateSchema,
|
||||
showEditSchemaCommentDialog,
|
||||
showCompileErrorDialog,
|
||||
compileErrorTitle,
|
||||
compileErrorMessage,
|
||||
schemaCommentText,
|
||||
schemaCommentLoading,
|
||||
schemaCommentPreviewSql,
|
||||
@@ -207,6 +210,7 @@ watch(
|
||||
showRedisDatabaseAliasDialog,
|
||||
showCreateSchemaDialog,
|
||||
showEditSchemaCommentDialog,
|
||||
showCompileErrorDialog,
|
||||
],
|
||||
(open) => {
|
||||
if (open.every((value) => !value)) emit("closed");
|
||||
@@ -806,5 +810,17 @@ watch(
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showCompileErrorDialog">
|
||||
<DialogContent class="sm:max-w-[560px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ compileErrorTitle }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<pre class="max-h-72 overflow-auto whitespace-pre-wrap break-words rounded bg-destructive/5 p-3 text-sm text-destructive">{{ compileErrorMessage }}</pre>
|
||||
<DialogFooter>
|
||||
<Button @click="showCompileErrorDialog = false">{{ t("common.close") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<SidebarMongoIndexManagerDialog v-if="showMongoIndexManagerDialog" :controller="controller" />
|
||||
</template>
|
||||
@@ -207,6 +207,7 @@ import { createDatabaseCollationOptionsForCharset, DEFAULT_GBASE8S_DATABASE_LOCA
|
||||
import { executeWithProductionContextGuard, executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
|
||||
import { connectionIsEffectivelyReadOnly } from "@/lib/database/readOnlyWriteAccess";
|
||||
import { buildXuguCompileSql } from "@/lib/database/xuguCompileSql";
|
||||
import { buildDamengCompileViewSql } from "@/lib/database/damengCompileSql";
|
||||
import type { SidebarDataOpenRequest } from "@/lib/sidebar/sidebarDataOpenCoordinator";
|
||||
import { createSidebarActionTarget, findSidebarActionTarget, releaseRemovedSidebarActionTarget, type SidebarActionTarget } from "@/lib/sidebar/sidebarActionTarget";
|
||||
import { createSidebarMenuContext, normalizeSidebarMenuDescriptors } from "@/lib/sidebar/sidebarTreeMenuDescriptors";
|
||||
@@ -333,6 +334,9 @@ import {
|
||||
editDatabaseCollation,
|
||||
editDatabaseCommentText,
|
||||
showEditSchemaCommentDialog,
|
||||
showCompileErrorDialog,
|
||||
compileErrorTitle,
|
||||
compileErrorMessage,
|
||||
schemaCommentText,
|
||||
schemaCommentLoading,
|
||||
schemaCommentPreviewSql,
|
||||
@@ -2617,6 +2621,26 @@ async function compileXuguObject() {
|
||||
}
|
||||
}
|
||||
|
||||
async function compileDamengView() {
|
||||
const node = activeNode.value;
|
||||
if (currentDatabaseType() !== "dameng" || node.type !== "view" || !node.connectionId || !node.database) return;
|
||||
const sql = buildDamengCompileViewSql({ 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.refreshObjectListTreeNode(node.connectionId, node.database, node.schema);
|
||||
} catch (e: any) {
|
||||
compileErrorTitle.value = t("contextMenu.compileObjectFailedTitle");
|
||||
compileErrorMessage.value = t("contextMenu.compileObjectFailedMessage", { name: node.label, message: e?.message || String(e) });
|
||||
claimTreeItemDialogOwnership();
|
||||
routeTreeItemDialogController();
|
||||
showCompileErrorDialog.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeXuguSchedulerJobAction(action: XuguSchedulerJobAction) {
|
||||
const node = activeNode.value;
|
||||
if (currentDatabaseType() !== "xugu" || node.type !== "job" || !node.connectionId || !node.database) return;
|
||||
@@ -5291,6 +5315,9 @@ function databaseSpecificDialogCapabilities() {
|
||||
createSchemaName,
|
||||
confirmCreateSchema,
|
||||
showEditSchemaCommentDialog,
|
||||
showCompileErrorDialog,
|
||||
compileErrorTitle,
|
||||
compileErrorMessage,
|
||||
schemaCommentText,
|
||||
schemaCommentLoading,
|
||||
schemaCommentPreviewSql,
|
||||
@@ -6093,6 +6120,9 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
if (node.type === "view" || node.type === "materialized_view") {
|
||||
items.push({ label: t("contextMenu.editView"), action: () => openObjectSourceDialog(true), icon: Pencil });
|
||||
items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 });
|
||||
if (node.type === "view" && currentDatabaseType() === "dameng" && buildDamengCompileViewSql({ schema: node.schema, name: node.objectName || node.label })) {
|
||||
items.push({ label: t("contextMenu.compileObject"), action: compileDamengView, icon: Wrench });
|
||||
}
|
||||
items.push({
|
||||
label: t("contextMenu.viewDdl"),
|
||||
action: openDdl,
|
||||
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const source = readFileSync(new URL("../SidebarTreeRuntimeHost.vue", import.meta.url), "utf8");
|
||||
|
||||
function functionSource(name: string, nextName: string) {
|
||||
const start = source.indexOf(`async function ${name}`);
|
||||
const end = source.indexOf(`async function ${nextName}`, start);
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
describe("SidebarTreeRuntimeHost compile refresh routing", () => {
|
||||
it("refreshes the Dameng schema object list after compiling a view", () => {
|
||||
const compile = functionSource("compileDamengView", "executeXuguSchedulerJobAction");
|
||||
|
||||
expect(compile).toContain("await connectionStore.refreshObjectListTreeNode(node.connectionId, node.database, node.schema);");
|
||||
expect(compile).not.toContain("await connectionStore.refreshTreeNode(node);");
|
||||
});
|
||||
|
||||
it("keeps Xugu compile refresh behavior unchanged", () => {
|
||||
const compile = functionSource("compileXuguObject", "compileDamengView");
|
||||
|
||||
expect(compile).toContain("await connectionStore.refreshTreeNode(node);");
|
||||
expect(compile).not.toContain("await connectionStore.refreshObjectListTreeNode");
|
||||
});
|
||||
});
|
||||
@@ -184,6 +184,9 @@ export const editDatabaseCharset = ref("utf8mb4");
|
||||
export const editDatabaseCollation = ref("utf8mb4_unicode_ci");
|
||||
export const editDatabaseCommentText = ref("");
|
||||
export const showEditSchemaCommentDialog = ref(false);
|
||||
export const showCompileErrorDialog = ref(false);
|
||||
export const compileErrorTitle = ref("");
|
||||
export const compileErrorMessage = ref("");
|
||||
export const schemaCommentText = ref("");
|
||||
export const schemaCommentLoading = ref(false);
|
||||
export const schemaCommentPreviewSql = ref("");
|
||||
@@ -228,12 +231,15 @@ const openFlags = [
|
||||
showDropSchemaConfirm,
|
||||
showEditDatabasePropertiesDialog,
|
||||
showEditSchemaCommentDialog,
|
||||
showCompileErrorDialog,
|
||||
showDeleteGroupConfirm,
|
||||
showMoveToNewGroupDialog,
|
||||
];
|
||||
|
||||
export function resetSidebarTreeDialogState() {
|
||||
for (const flag of openFlags) flag.value = false;
|
||||
compileErrorTitle.value = "";
|
||||
compileErrorMessage.value = "";
|
||||
createDatabaseUsers.value = [];
|
||||
createDatabaseSelectedUsers.value = [];
|
||||
createDatabaseUsersLoading.value = false;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import en from "@/i18n/locales/en";
|
||||
import es from "@/i18n/locales/es";
|
||||
import itLocale from "@/i18n/locales/it";
|
||||
import ja from "@/i18n/locales/ja";
|
||||
import ko from "@/i18n/locales/ko";
|
||||
import ptBR from "@/i18n/locales/pt-BR";
|
||||
import zhCN from "@/i18n/locales/zh-CN";
|
||||
import zhTW from "@/i18n/locales/zh-TW";
|
||||
|
||||
type ViewMessages = {
|
||||
contextMenu: { compileObjectFailedTitle: string; compileObjectFailedMessage: string };
|
||||
objects: { validStatus: string; invalidStatus: string };
|
||||
};
|
||||
|
||||
const locales = { en, es, it: itLocale, ja, ko, "pt-BR": ptBR, "zh-CN": zhCN, "zh-TW": zhTW } as unknown as Record<string, ViewMessages>;
|
||||
|
||||
describe.each(Object.entries(locales))("Dameng view messages: %s", (localeName, messages) => {
|
||||
it("provides a localized compilation failure with both placeholders", () => {
|
||||
expect(messages.contextMenu.compileObjectFailedTitle).toBeTypeOf("string");
|
||||
expect(messages.contextMenu.compileObjectFailedMessage).toContain("{name}");
|
||||
expect(messages.contextMenu.compileObjectFailedMessage).toContain("{message}");
|
||||
expect(messages.contextMenu.compileObjectFailedMessage).toContain("\n");
|
||||
if (localeName !== "en") expect(messages.contextMenu.compileObjectFailedTitle).not.toBe(en.contextMenu.compileObjectFailedTitle);
|
||||
});
|
||||
|
||||
it("preserves the database validity labels", () => {
|
||||
expect(messages.objects.validStatus).toBe("VALID");
|
||||
expect(messages.objects.invalidStatus).toBe("INVALID");
|
||||
});
|
||||
});
|
||||
@@ -3599,6 +3599,8 @@ export default {
|
||||
executeProcedure: "Execute Procedure",
|
||||
compileObject: "Compile",
|
||||
compileObjectSuccess: '"{name}" compiled successfully',
|
||||
compileObjectFailedTitle: "View compilation failed",
|
||||
compileObjectFailedMessage: 'View "{name}" compilation failed:\n{message}',
|
||||
confirmExecuteProcedureTitle: "Execute Procedure",
|
||||
confirmExecuteProcedureMessage: 'Execute procedure "{name}"? You can fill or adjust parameter values first.',
|
||||
loadingProcedureParameters: "Loading parameters...",
|
||||
@@ -4342,6 +4344,8 @@ export default {
|
||||
validity: "Validity",
|
||||
valid: "Valid",
|
||||
invalid: "Invalid",
|
||||
validStatus: "VALID",
|
||||
invalidStatus: "INVALID",
|
||||
sequence: "Sequence",
|
||||
package: "Package",
|
||||
packageBody: "Package Body",
|
||||
|
||||
@@ -3231,6 +3231,8 @@ export default withEnglishFallback({
|
||||
maxRetriesHint: "Reintentar automáticamente en caso de limitación de tasa, tiempo de espera o error de red temporal. 0 = no reintentar, máximo 10.",
|
||||
},
|
||||
contextMenu: {
|
||||
compileObjectFailedTitle: "Error al compilar la vista",
|
||||
compileObjectFailedMessage: 'No se pudo compilar la vista "{name}":\n{message}',
|
||||
openConnection: "Abrir conexión",
|
||||
closeConnection: "Cerrar conexión",
|
||||
closeSelectedConnections: "Cerrar {count} conexiones seleccionadas",
|
||||
@@ -4017,6 +4019,8 @@ export default withEnglishFallback({
|
||||
anyPrivilegeDisabled: "Los privilegios de tipo ANY están deshabilitados por DM8 (ENABLE_DDL_ANY_PRIV=0) y no se pueden otorgar ni revocar",
|
||||
},
|
||||
objects: {
|
||||
validStatus: "VALID",
|
||||
invalidStatus: "INVALID",
|
||||
all: "Todos",
|
||||
tables: "Tablas",
|
||||
collections: "Colecciones",
|
||||
|
||||
@@ -3229,6 +3229,8 @@ export default withEnglishFallback({
|
||||
maxRetriesHint: "Riprova automaticamente in caso di limitazione, timeout o errore di rete temporaneo. 0 = nessun tentativo, massimo 10.",
|
||||
},
|
||||
contextMenu: {
|
||||
compileObjectFailedTitle: "Compilazione della vista non riuscita",
|
||||
compileObjectFailedMessage: 'Compilazione della vista "{name}" non riuscita:\n{message}',
|
||||
openConnection: "Apri Connessione",
|
||||
closeConnection: "Chiudi Connessione",
|
||||
closeSelectedConnections: "Chiudi {count} connessioni selezionate",
|
||||
@@ -4015,6 +4017,8 @@ export default withEnglishFallback({
|
||||
anyPrivilegeDisabled: "I privilegi di tipo ANY sono disabilitati da DM8 (ENABLE_DDL_ANY_PRIV=0) e non possono essere concessi o revocati",
|
||||
},
|
||||
objects: {
|
||||
validStatus: "VALID",
|
||||
invalidStatus: "INVALID",
|
||||
all: "Tutti",
|
||||
tables: "Tabelle",
|
||||
collections: "Collezioni",
|
||||
|
||||
@@ -3255,6 +3255,8 @@ export default withEnglishFallback({
|
||||
maxRetriesHint: "レート制限、タイムアウト、または一時的なネットワークエラーが発生した場合に自動的にリトライします。0 = リトライなし、最大10。",
|
||||
},
|
||||
contextMenu: {
|
||||
compileObjectFailedTitle: "ビューのコンパイルに失敗しました",
|
||||
compileObjectFailedMessage: "ビュー「{name}」のコンパイルに失敗しました:\n{message}",
|
||||
openConnection: "接続を開く",
|
||||
closeConnection: "接続を閉じる",
|
||||
closeSelectedConnections: "選択した{count}件の接続を閉じる",
|
||||
@@ -4077,6 +4079,8 @@ export default withEnglishFallback({
|
||||
anyPrivilegeDisabled: "ANY系権限はDM8で無効化されています(ENABLE_DDL_ANY_PRIV=0)ため、付与・取り消しはできません",
|
||||
},
|
||||
objects: {
|
||||
validStatus: "VALID",
|
||||
invalidStatus: "INVALID",
|
||||
all: "すべて",
|
||||
tables: "テーブル",
|
||||
collections: "コレクション",
|
||||
|
||||
@@ -3129,6 +3129,8 @@ export default withEnglishFallback({
|
||||
noDatabasesFound: "데이터베이스를 찾을 수 없습니다",
|
||||
},
|
||||
contextMenu: {
|
||||
compileObjectFailedTitle: "뷰 컴파일 실패",
|
||||
compileObjectFailedMessage: '뷰 "{name}" 컴파일에 실패했습니다:\n{message}',
|
||||
openConnection: "연결 열기",
|
||||
closeConnection: "연결 닫기",
|
||||
closeSelectedConnections: "선택한 연결 {count}개 닫기",
|
||||
@@ -4004,6 +4006,8 @@ export default withEnglishFallback({
|
||||
anyPrivilegeDisabled: "ANY 유형 권한은 DM8에서 비활성화되어(ENABLE_DDL_ANY_PRIV=0) 부여하거나 회수할 수 없습니다",
|
||||
},
|
||||
objects: {
|
||||
validStatus: "VALID",
|
||||
invalidStatus: "INVALID",
|
||||
all: "전체",
|
||||
tables: "테이블",
|
||||
collections: "컬렉션",
|
||||
|
||||
@@ -3231,6 +3231,8 @@ export default withEnglishFallback({
|
||||
maxRetriesHint: "Tente novamente automaticamente ao encontrar limitação de taxa, tempo limite ou erro de rede temporário. 0 = não tentar novamente, máximo 10.",
|
||||
},
|
||||
contextMenu: {
|
||||
compileObjectFailedTitle: "Falha na compilação da visão",
|
||||
compileObjectFailedMessage: 'Falha ao compilar a visão "{name}":\n{message}',
|
||||
openConnection: "Abrir Conexão",
|
||||
closeConnection: "Fechar Conexão",
|
||||
closeSelectedConnections: "Fechar {count} conexões selecionadas",
|
||||
@@ -4017,6 +4019,8 @@ export default withEnglishFallback({
|
||||
anyPrivilegeDisabled: "Privilégios do tipo ANY estão desabilitados pelo DM8 (ENABLE_DDL_ANY_PRIV=0) e não podem ser concedidos ou revogados",
|
||||
},
|
||||
objects: {
|
||||
validStatus: "VALID",
|
||||
invalidStatus: "INVALID",
|
||||
all: "Todos",
|
||||
tables: "Tabelas",
|
||||
collections: "Coleções",
|
||||
|
||||
@@ -3513,6 +3513,8 @@ export default withEnglishFallback({
|
||||
executeProcedure: "执行过程",
|
||||
compileObject: "编译",
|
||||
compileObjectSuccess: '"{name}" 编译成功',
|
||||
compileObjectFailedTitle: "视图编译失败",
|
||||
compileObjectFailedMessage: "视图「{name}」编译失败:\n{message}",
|
||||
confirmExecuteProcedureTitle: "执行存储过程",
|
||||
confirmExecuteProcedureMessage: "确认执行存储过程「{name}」?可先补充或调整参数值。",
|
||||
loadingProcedureParameters: "正在读取参数...",
|
||||
@@ -4319,6 +4321,8 @@ export default withEnglishFallback({
|
||||
validity: "有效性",
|
||||
valid: "有效",
|
||||
invalid: "无效",
|
||||
validStatus: "VALID",
|
||||
invalidStatus: "INVALID",
|
||||
sequence: "序列",
|
||||
package: "包",
|
||||
packageBody: "包体",
|
||||
|
||||
@@ -3228,6 +3228,8 @@ export default withEnglishFallback({
|
||||
noDatabasesFound: "未找到資料庫",
|
||||
},
|
||||
contextMenu: {
|
||||
compileObjectFailedTitle: "檢視編譯失敗",
|
||||
compileObjectFailedMessage: "檢視「{name}」編譯失敗:\n{message}",
|
||||
openConnection: "開啟連線",
|
||||
closeConnection: "關閉連線",
|
||||
closeSelectedConnections: "關閉選取的 {count} 個連線",
|
||||
@@ -3762,6 +3764,8 @@ export default withEnglishFallback({
|
||||
buckets: "Buckets",
|
||||
},
|
||||
objects: {
|
||||
validStatus: "VALID",
|
||||
invalidStatus: "INVALID",
|
||||
all: "全部",
|
||||
tables: "資料表",
|
||||
collections: "集合",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildDamengCompileViewSql } from "@/lib/database/damengCompileSql";
|
||||
|
||||
describe("buildDamengCompileViewSql", () => {
|
||||
it("quotes schema and view identifiers", () => {
|
||||
expect(buildDamengCompileViewSql({ schema: "APP", name: "ACTIVE_VIEW" })).toBe('ALTER VIEW "APP"."ACTIVE_VIEW" COMPILE;');
|
||||
});
|
||||
|
||||
it("escapes embedded double quotes", () => {
|
||||
expect(buildDamengCompileViewSql({ schema: 'APP"DATA', name: 'V"ACTIVE' })).toBe('ALTER VIEW "APP""DATA"."V""ACTIVE" COMPILE;');
|
||||
});
|
||||
|
||||
it("allows a database-level name when no schema is supplied", () => {
|
||||
expect(buildDamengCompileViewSql({ name: "ACTIVE_VIEW" })).toBe('ALTER VIEW "ACTIVE_VIEW" COMPILE;');
|
||||
});
|
||||
|
||||
it("rejects a blank view name", () => {
|
||||
expect(buildDamengCompileViewSql({ schema: "APP", name: " " })).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,20 @@ describe("buildObjectBrowserRows", () => {
|
||||
|
||||
expect(rows[0]?.schema).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves nullable view validity in browser rows", () => {
|
||||
const rows = buildObjectBrowserRows({
|
||||
objects: [
|
||||
{ name: "valid_view", object_type: "VIEW", valid: true },
|
||||
{ name: "invalid_view", object_type: "VIEW", valid: false },
|
||||
{ name: "unknown_view", object_type: "VIEW", valid: null },
|
||||
],
|
||||
database: "app",
|
||||
fallbackSchema: "app",
|
||||
});
|
||||
|
||||
expect(rows.map((row) => row.valid)).toEqual([true, false, null]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Object Browser pinned ordering", () => {
|
||||
|
||||
@@ -38,6 +38,40 @@ describe("case-sensitive database objects", () => {
|
||||
expect(new Set(viewGroup?.children?.map((node) => node.id) ?? []).size).toBe(2);
|
||||
});
|
||||
|
||||
it("preserves view validity in simple and grouped trees", () => {
|
||||
const objects: ObjectInfo[] = [
|
||||
{ name: "VALID_VIEW", object_type: "VIEW", schema: "dbx_test", valid: true },
|
||||
{ name: "INVALID_VIEW", object_type: "VIEW", schema: "dbx_test", valid: false },
|
||||
];
|
||||
|
||||
const simple = buildSimpleObjectTreeNodes({ ...context, schema: "dbx_test", objects });
|
||||
expect(simple.map((node) => node.valid)).toEqual([false, true]);
|
||||
|
||||
const grouped = buildGroupedObjectTreeNodes({ ...context, schema: "dbx_test", objects });
|
||||
expect(grouped.find((node) => node.type === "group-views")?.children?.map((node) => node.valid)).toEqual([false, true]);
|
||||
});
|
||||
|
||||
it("preserves view validity when building the table-like tree path", () => {
|
||||
const tables: TableInfo[] = [
|
||||
{ name: "VALID_VIEW", table_type: "VIEW", valid: true },
|
||||
{ name: "INVALID_VIEW", table_type: "VIEW", valid: false },
|
||||
];
|
||||
const nodes = buildTableTreeNodes({
|
||||
...context,
|
||||
schema: "dbx_test",
|
||||
tables,
|
||||
});
|
||||
|
||||
expect(nodes.map((node) => node.valid)).toEqual([false, true]);
|
||||
|
||||
const grouped = buildGroupedObjectTreeNodes({
|
||||
...context,
|
||||
schema: "dbx_test",
|
||||
objects: mergeTableInfosIntoObjects([], tables, "dbx_test"),
|
||||
});
|
||||
expect(grouped.find((node) => node.type === "group-views")?.children?.map((node) => node.valid)).toEqual([false, true]);
|
||||
});
|
||||
|
||||
it("keeps table nodes whose names differ only by case across pages", () => {
|
||||
const firstPage = buildTableTreeNodes({
|
||||
...context,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export interface DamengCompileViewSqlInput {
|
||||
schema?: string | null;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function buildDamengCompileViewSql(input: DamengCompileViewSqlInput): string | null {
|
||||
const name = input.name.trim();
|
||||
if (!name) return null;
|
||||
const schema = input.schema?.trim();
|
||||
const qualifiedName = schema ? `${quoteDamengIdentifier(schema)}.${quoteDamengIdentifier(name)}` : quoteDamengIdentifier(name);
|
||||
return `ALTER VIEW ${qualifiedName} COMPILE;`;
|
||||
}
|
||||
|
||||
function quoteDamengIdentifier(value: string): string {
|
||||
return `"${value.replaceAll('"', '""')}"`;
|
||||
}
|
||||
@@ -121,6 +121,7 @@ export function objectBrowserRowLegacyPinnedTreeNodeIds(row: ObjectBrowserRow, c
|
||||
{
|
||||
name: row.name,
|
||||
table_type: row.type,
|
||||
valid: row.valid,
|
||||
comment: row.comment,
|
||||
parent_schema: row.partitionParentSchema,
|
||||
parent_name: row.partitionParentName,
|
||||
|
||||
@@ -24,6 +24,7 @@ export function buildTableTreeNodes({ nodeId, connectionId, database, schema, ta
|
||||
objectType,
|
||||
tableType: table.table_type,
|
||||
comment: table.comment,
|
||||
valid: table.valid,
|
||||
parentSchema: table.parent_schema,
|
||||
parentName: table.parent_name,
|
||||
catalog,
|
||||
@@ -53,6 +54,7 @@ function makeTableTreeEntry({
|
||||
objectType,
|
||||
tableType,
|
||||
comment,
|
||||
valid,
|
||||
parentSchema,
|
||||
parentName,
|
||||
catalog,
|
||||
@@ -66,6 +68,7 @@ function makeTableTreeEntry({
|
||||
objectType: DatabaseObjectTreeKind;
|
||||
tableType?: string;
|
||||
comment?: string | null;
|
||||
valid?: boolean | null;
|
||||
parentSchema?: string | null;
|
||||
parentName?: string | null;
|
||||
catalog?: string;
|
||||
@@ -79,6 +82,7 @@ function makeTableTreeEntry({
|
||||
type: objectType === "VIEW" ? ("view" as const) : objectType === "MATERIALIZED_VIEW" ? ("materialized_view" as const) : ("table" as const),
|
||||
tableType,
|
||||
comment,
|
||||
valid,
|
||||
connectionId,
|
||||
database,
|
||||
schema,
|
||||
@@ -248,6 +252,9 @@ export function mergeTableInfosIntoObjects(objects: readonly ObjectInfo[], table
|
||||
if (matchingObject && table.comment && !matchingObject.comment) {
|
||||
matchingObject.comment = table.comment;
|
||||
}
|
||||
if (matchingObject && matchingObject.valid == null && table.valid != null) {
|
||||
matchingObject.valid = table.valid;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
@@ -257,6 +264,7 @@ export function mergeTableInfosIntoObjects(objects: readonly ObjectInfo[], table
|
||||
// downstream template strategies can distinguish special table kinds.
|
||||
object_type: table.table_type,
|
||||
schema: tableSchema,
|
||||
valid: table.valid,
|
||||
comment: table.comment,
|
||||
created_at: undefined,
|
||||
updated_at: undefined,
|
||||
@@ -541,6 +549,7 @@ function buildObjectTreeEntries({ nodeId, connectionId, database, schema, object
|
||||
objectType,
|
||||
tableType: obj.object_type,
|
||||
comment: obj.comment,
|
||||
valid: obj.valid,
|
||||
parentSchema: obj.parent_schema,
|
||||
parentName: obj.parent_name,
|
||||
}),
|
||||
@@ -658,6 +667,7 @@ export function buildSimpleObjectTreeNodes({ nodeId, connectionId, database, sch
|
||||
objectType,
|
||||
tableType: obj.object_type,
|
||||
comment: obj.comment,
|
||||
valid: obj.valid,
|
||||
parentSchema: obj.parent_schema,
|
||||
parentName: obj.parent_name,
|
||||
});
|
||||
|
||||
@@ -789,6 +789,8 @@ export interface CatalogInfo {
|
||||
export interface TableInfo {
|
||||
name: string;
|
||||
table_type: string;
|
||||
/** Optional validity populated by status-aware object loaders. */
|
||||
valid?: boolean | null;
|
||||
comment?: string | null;
|
||||
parent_schema?: string | null;
|
||||
parent_name?: string | null;
|
||||
|
||||
Reference in new issue
Block a user