fix: distinguish materialized view from regular view

This commit is contained in:
t8y2 committed 2026-06-16 21:23:42 +08:00
1 parent c619e5da56
commit 8339d23df5
16 files changed
+92 -57

No files matched your search

@@ -393,7 +393,7 @@ async function loadDiagram() {
await store.ensureConnected(connectionId.value);
const querySchema = schema.value || database.value;
const tableInfos = await api.listTables(connectionId.value, database.value, querySchema);
const baseTables = tableInfos.filter((table) => table.table_type !== "VIEW").sort((a, b) => a.name.localeCompare(b.name));
const baseTables = tableInfos.filter((table) => table.table_type !== "VIEW" && table.table_type !== "MATERIALIZED VIEW").sort((a, b) => a.name.localeCompare(b.name));
totalTableCount.value = baseTables.length;
const loadedTables: DiagramTable[] = [];
@@ -357,7 +357,7 @@ async function loadTables(side: "source" | "target") {
const database = side === "source" ? sourceDatabase.value : targetDatabase.value;
if (!connectionId || !database) return;
const schema = side === "source" ? sourceSchema.value || (await resolveSchema(connectionId, database, props.prefillSchema)) : targetSchema.value || (await resolveSchema(connectionId, database));
const tables = (await api.listTables(connectionId, database, schema)).filter((table) => table.table_type !== "VIEW").map((table) => table.name);
const tables = (await api.listTables(connectionId, database, schema)).filter((table) => table.table_type !== "VIEW" && table.table_type !== "MATERIALIZED VIEW").map((table) => table.name);
if (side === "source") {
const preferredSelection = props.prefillTable && tables.includes(props.prefillTable) ? [props.prefillTable] : [...selectedSourceTables.value].filter((table) => tables.includes(table));
@@ -394,7 +394,7 @@ async function ensureTableObjectGroupsLoaded(target: Extract<ActiveTabSidebarTar
function findTableObjectGroupNodes(nodes: TreeNode[], target: Extract<ActiveTabSidebarTarget, { type: "table" }>): TreeNode[] {
const matches: TreeNode[] = [];
for (const node of nodes) {
if ((node.type === "group-tables" || node.type === "group-views") && node.connectionId === target.connectionId && sameTreeName(node.database, target.database) && (!target.schema || sameTreeName(node.schema, target.schema))) {
if ((node.type === "group-tables" || node.type === "group-views" || node.type === "group-materialized-views") && node.connectionId === target.connectionId && sameTreeName(node.database, target.database) && (!target.schema || sameTreeName(node.schema, target.schema))) {
matches.push(node);
}
if (node.children) {
@@ -201,6 +201,8 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
return { icon: Table, colorClass: "text-green-500" };
case "view":
return { icon: Eye, colorClass: "text-purple-500" };
case "materialized_view":
return { icon: Eye, colorClass: "text-indigo-500" };
case "column":
if ((node.meta as ColumnInfo).is_primary_key) {
return { icon: Columns3, colorClass: "text-orange-400" };
@@ -249,6 +251,8 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
return { icon: Table, colorClass: "text-green-500" };
case "group-views":
return { icon: Eye, colorClass: "text-purple-500" };
case "group-materialized-views":
return { icon: Eye, colorClass: "text-indigo-500" };
case "group-procedures":
return { icon: ScrollText, colorClass: "text-blue-500" };
case "group-functions":
@@ -266,8 +270,8 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
}
}
const groupTypes: Set<TreeNodeType> = new Set(["group-columns", "group-indexes", "group-fkeys", "group-triggers", "group-tables", "group-views", "group-procedures", "group-functions", "group-sequences", "group-packages", "group-partitions"]);
const pinnableTypes: Set<TreeNodeType> = new Set(["connection-group", "database", "schema", "table", "view", "redis-db", "mongo-db", "mongo-collection", "elasticsearch-index"]);
const groupTypes: Set<TreeNodeType> = new Set(["group-columns", "group-indexes", "group-fkeys", "group-triggers", "group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages", "group-partitions"]);
const pinnableTypes: Set<TreeNodeType> = new Set(["connection-group", "database", "schema", "table", "view", "materialized_view", "redis-db", "mongo-db", "mongo-collection", "elasticsearch-index"]);
function isGroupLabel(node: TreeNode): boolean {
return groupTypes.has(node.type);
@@ -282,7 +286,7 @@ function displayLabel(node: TreeNode): string {
}
function visibleLabel(node: TreeNode): string {
if (node.type === "table" || node.type === "view" || node.type === "mongo-collection" || node.type === "elasticsearch-index") {
if (node.type === "table" || node.type === "view" || node.type === "materialized_view" || node.type === "mongo-collection" || node.type === "elasticsearch-index") {
return sidebarDisplayTableName(node.label, settingsStore.editorSettings.sidebarHiddenTablePrefixes);
}
return displayLabel(node);
@@ -326,7 +330,7 @@ async function toggle() {
return;
}
const databaseObjectGroup = node.type === "group-tables" || node.type === "group-views" || node.type === "group-procedures" || node.type === "group-functions" || node.type === "group-sequences" || node.type === "group-packages";
const databaseObjectGroup = node.type === "group-tables" || node.type === "group-views" || node.type === "group-materialized-views" || node.type === "group-procedures" || node.type === "group-functions" || node.type === "group-sequences" || node.type === "group-packages";
if (databaseObjectGroup && connectionStore.isTreeNodeChildrenLoaded(node.id)) {
node.isExpanded = !node.isExpanded;
emit("node-toggled", node, wasExpanded);
@@ -382,7 +386,7 @@ async function toggle() {
}
} else if (node.type === "schema" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.schema) {
await connectionStore.loadTables(node.connectionId, node.database, node.schema);
} else if ((node.type === "table" || node.type === "view") && node.connectionId && hasTreeNodeDatabaseContext(node)) {
} else if ((node.type === "table" || node.type === "view" || node.type === "materialized_view") && node.connectionId && hasTreeNodeDatabaseContext(node)) {
await connectionStore.loadTableGroups(node.connectionId, node.database, node.label, node.schema, node.id);
} else if (node.type === "group-columns" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
await connectionStore.loadColumns(node.connectionId, node.database, node.tableName, node.schema, node.id);
@@ -685,7 +689,7 @@ async function openUserAdmin() {
async function openData() {
const node = props.node;
if (!(node.type === "table" || node.type === "view") || !hasNodeDatabaseContext(node)) return;
if (!(node.type === "table" || node.type === "view" || node.type === "materialized_view") || !hasNodeDatabaseContext(node)) return;
const config = connectionStore.getConfig(node.connectionId);
const traceId = uuid().slice(0, 8);
const startedAt = performance.now();
@@ -730,7 +734,7 @@ async function openData() {
queryStore.setTableMeta(tabId, {
schema: tableSchema,
tableName: node.label,
tableType: node.type === "view" ? "VIEW" : "TABLE",
tableType: node.type === "view" ? "VIEW" : node.type === "materialized_view" ? "MATERIALIZED_VIEW" : "TABLE",
columns: [],
primaryKeys: [],
});
@@ -761,7 +765,7 @@ async function openData() {
});
columns = await api.getColumns(node.connectionId, node.database, querySchema, node.label);
if (!isActive()) return;
primaryKeys = editablePrimaryKeys(effectiveDbType, columns, node.type === "view" ? "VIEW" : "TABLE");
primaryKeys = editablePrimaryKeys(effectiveDbType, columns, node.type === "view" ? "VIEW" : node.type === "materialized_view" ? "MATERIALIZED_VIEW" : "TABLE");
console.info("[DBX][openData:get-columns:done]", {
traceId,
columnCount: columns.length,
@@ -771,7 +775,7 @@ async function openData() {
queryStore.setTableMeta(tabId, {
schema: tableSchema,
tableName: node.label,
tableType: node.type === "view" ? "VIEW" : "TABLE",
tableType: node.type === "view" ? "VIEW" : node.type === "materialized_view" ? "MATERIALIZED_VIEW" : "TABLE",
columns,
primaryKeys,
});
@@ -822,7 +826,7 @@ async function newQuery() {
if (hasTreeNodeDatabaseContext(node)) {
const tabId = queryStore.createTab(node.connectionId, node.database, undefined, "query", node.schema);
// For table/view nodes, generate SELECT * FROM table_name SQL
if (node.type === "table" || node.type === "view") {
if (node.type === "table" || node.type === "view" || node.type === "materialized_view") {
const config = connectionStore.getConfig(node.connectionId);
const dbType = config ? effectiveDatabaseTypeForConnection(config) : undefined;
const tableSchema = node.schema || node.database;
@@ -1234,10 +1238,10 @@ function dropObjectSqlOptions(): DropObjectSqlOptions | null {
}
function dropObjectSqlOptionsForNode(node: TreeNode): DropObjectSqlOptions | null {
if (node.type !== "view" && node.type !== "procedure" && node.type !== "function") return null;
if (node.type !== "view" && node.type !== "materialized_view" && node.type !== "procedure" && node.type !== "function") return null;
return {
databaseType: tableStructureDatabaseTypeForNode(node),
objectType: node.type === "view" ? "VIEW" : node.type === "procedure" ? "PROCEDURE" : "FUNCTION",
objectType: node.type === "view" ? "VIEW" : node.type === "materialized_view" ? "MATERIALIZED_VIEW" : node.type === "procedure" ? "PROCEDURE" : "FUNCTION",
schema: node.schema,
name: node.label,
};
@@ -1292,6 +1296,7 @@ function canDropTableChildObjectNode(node: TreeNode): boolean {
function dropObjectMenuLabel(): string {
if (props.node.type === "view") return t("contextMenu.dropView");
if (props.node.type === "materialized_view") return t("contextMenu.dropView");
if (props.node.type === "procedure") return t("contextMenu.dropProcedure");
if (props.node.type === "function") return t("contextMenu.dropFunction");
return t("contextMenu.dropObject");
@@ -1299,6 +1304,7 @@ function dropObjectMenuLabel(): string {
function dropObjectConfirmTitle(): string {
if (props.node.type === "view") return t("contextMenu.confirmDropViewTitle");
if (props.node.type === "materialized_view") return t("contextMenu.confirmDropViewTitle");
if (props.node.type === "procedure") return t("contextMenu.confirmDropProcedureTitle");
if (props.node.type === "function") return t("contextMenu.confirmDropFunctionTitle");
return t("contextMenu.confirmDropObjectTitle");
@@ -1306,6 +1312,7 @@ function dropObjectConfirmTitle(): string {
function dropObjectConfirmMessage(): string {
if (props.node.type === "view") return t("contextMenu.confirmDropViewMessage", { name: props.node.label });
if (props.node.type === "materialized_view") return t("contextMenu.confirmDropViewMessage", { name: props.node.label });
if (props.node.type === "procedure") return t("contextMenu.confirmDropProcedureMessage", { name: props.node.label });
if (props.node.type === "function") return t("contextMenu.confirmDropFunctionMessage", { name: props.node.label });
return t("contextMenu.confirmDropObjectMessage", { name: props.node.label });
@@ -1376,13 +1383,14 @@ function viewObjectSource() {
function viewObjectDdl() {
const node = props.node;
if (node.type !== "view" || !node.connectionId || !node.database) return;
if ((node.type !== "view" && node.type !== "materialized_view") || !node.connectionId || !node.database) return;
const schema = node.schema || node.database;
const objectType = node.type === "materialized_view" ? "MATERIALIZED_VIEW" : "VIEW";
connectionStore
.ensureConnected(node.connectionId)
.then(() => {
connectionStore.activeConnectionId = node.connectionId!;
return api.getObjectSource(node.connectionId!, node.database!, schema, node.label, "VIEW");
return api.getObjectSource(node.connectionId!, node.database!, schema, node.label, objectType);
})
.then(async (result) => {
const connection = connectionStore.getConfig(node.connectionId!);
@@ -1434,7 +1442,7 @@ function requestDropTableChildObject() {
function canDropTreeNode(node: TreeNode): boolean {
if (node.type === "table") return !!node.connectionId && !!node.database;
if (node.type === "view" || node.type === "procedure" || node.type === "function") {
if (node.type === "view" || node.type === "materialized_view" || node.type === "procedure" || node.type === "function") {
return !!node.connectionId && !!node.database && !!dropObjectSqlOptionsForNode(node);
}
return canDropTableChildObjectNode(node);
@@ -1523,6 +1531,7 @@ function requestDropSelectedNode(): boolean {
function nodeRenameObjectType(): RenameableObjectType | null {
if (props.node.type === "table") return "TABLE";
if (props.node.type === "view") return "VIEW";
if (props.node.type === "materialized_view") return "MATERIALIZED_VIEW";
if (props.node.type === "procedure") return "PROCEDURE";
if (props.node.type === "function") return "FUNCTION";
return null;
@@ -1622,9 +1631,9 @@ async function confirmDropObject() {
await connectionStore.ensureConnected(node.connectionId);
const sql = dropObjectPreviewSql.value || (await buildDropObjectSql(options));
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
const msgKey = node.type === "view" ? "contextMenu.dropViewSuccess" : node.type === "procedure" ? "contextMenu.dropProcedureSuccess" : "contextMenu.dropFunctionSuccess";
const msgKey = node.type === "view" ? "contextMenu.dropViewSuccess" : node.type === "materialized_view" ? "contextMenu.dropViewSuccess" : node.type === "procedure" ? "contextMenu.dropProcedureSuccess" : "contextMenu.dropFunctionSuccess";
toast(t(msgKey, { name: node.label }), 3000);
if (node.type === "view") {
if (node.type === "view" || node.type === "materialized_view") {
connectionStore.removeTreeNode(node.id);
} else {
await refreshTableList(node);
@@ -2528,8 +2537,8 @@ function openDatabaseExport() {
connectionStore.databaseExportSource = {
connectionId: node.connectionId,
database: node.database,
schema: node.type === "schema" || node.type === "table" || node.type === "view" ? node.schema : undefined,
tableName: node.type === "table" || node.type === "view" ? node.label : undefined,
schema: node.type === "schema" || node.type === "table" || node.type === "view" || node.type === "materialized_view" ? node.schema : undefined,
tableName: node.type === "table" || node.type === "view" || node.type === "materialized_view" ? node.label : undefined,
};
}
@@ -2782,7 +2791,7 @@ const TABLE_REFERENCE_DRAG_THRESHOLD = 5;
const TABLE_REFERENCE_DRAGGING_CLASS = "dbx-table-reference-dragging";
const canDragTableReference = computed(() => {
if (props.dragDisabled || !props.node.connectionId || props.node.database == null) return false;
if (props.node.type === "table" || props.node.type === "view") return true;
if (props.node.type === "table" || props.node.type === "view" || props.node.type === "materialized_view") return true;
return props.node.type === "column" && !!props.node.tableName;
});
@@ -3209,8 +3218,8 @@ function treeItemMenuItems(): ContextMenuItem[] {
return items;
}
// 6. Table / View
if (node.type === "table" || node.type === "view") {
// 6. Table / View / Materialized View
if (node.type === "table" || node.type === "view" || node.type === "materialized_view") {
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.viewData"), action: openData, icon: TableProperties });
@@ -3224,7 +3233,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
icon: FileCode,
});
}
if (node.type === "view") {
if (node.type === "view" || node.type === "materialized_view") {
items.push({ label: t("contextMenu.editView"), action: viewObjectSource, icon: Pencil });
items.push({ label: t("contextMenu.viewSource"), action: viewObjectSource, icon: Code2 });
items.push({ label: t("contextMenu.viewDdl"), action: viewObjectDdl, icon: FileCode });
@@ -3240,7 +3249,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
shortcut: shortcutRename,
});
}
if (node.type === "view") {
if (node.type === "view" || node.type === "materialized_view") {
items.push({
label: deleteMenuLabel(t("contextMenu.dropView")),
action: deleteMenuAction(requestDropObject),
@@ -3475,7 +3484,10 @@ function treeItemMenuItems(): ContextMenuItem[] {
/>
<span v-else ref="labelRef" :class="labelWidthClass">{{ visibleLabel(node) }}</span>
<span
v-if="(node.type === 'group-tables' || node.type === 'group-views' || node.type === 'group-procedures' || node.type === 'group-functions' || node.type === 'group-sequences' || node.type === 'group-packages' || node.type === 'group-partitions') && node.objectCount != null"
v-if="
(node.type === 'group-tables' || node.type === 'group-views' || node.type === 'group-materialized-views' || node.type === 'group-procedures' || node.type === 'group-functions' || node.type === 'group-sequences' || node.type === 'group-packages' || node.type === 'group-partitions') &&
node.objectCount != null
"
class="text-muted-foreground text-[10px] shrink-0"
>{{ node.objectCount }}</span
>