feat(dameng): show view validity and support recompilation

This commit is contained in:
guan xin authored and GitHub committed 2026-09-17 19:30:44 +08:00
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,
@@ -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");
});
});
+4
View File
@@ -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",
+4
View File
@@ -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",
+4
View File
@@ -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",
+4
View File
@@ -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: "コレクション",
+4
View File
@@ -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: "컬렉션",
+4
View File
@@ -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",
+4
View File
@@ -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: "包体",
+4
View File
@@ -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: "集合",
+20
View File
@@ -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", () => {
+34
View File
@@ -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,
+10
View File
@@ -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,
});
+2
View File
@@ -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;