From 8339d23df5790406f750fcb2a1c351a447a0bcac Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 16 Jun 2026 21:23:42 +0800 Subject: [PATCH] fix: distinguish materialized view from regular view --- .../diagram/SchemaDiagramDialog.vue | 2 +- .../src/components/diff/DataCompareDialog.vue | 2 +- .../src/components/sidebar/ConnectionTree.vue | 2 +- .../src/components/sidebar/TreeItem.vue | 62 +++++++++++-------- apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/zh-CN.ts | 1 + .../src/lib/databaseObjectCapabilities.ts | 10 ++- apps/desktop/src/lib/objectBrowserRows.ts | 3 +- apps/desktop/src/lib/objectRenameSql.ts | 4 +- .../desktop/src/lib/sidebarActiveTabTarget.ts | 2 +- apps/desktop/src/lib/sidebarTreeItemLayout.ts | 2 +- apps/desktop/src/lib/tableTree.ts | 22 ++++--- apps/desktop/src/lib/treeNodeClick.ts | 7 ++- apps/desktop/src/stores/connectionStore.ts | 16 ++--- apps/desktop/src/types/database.ts | 6 +- crates/dbx-core/src/db_admin_sql.rs | 7 ++- 16 files changed, 92 insertions(+), 57 deletions(-) diff --git a/apps/desktop/src/components/diagram/SchemaDiagramDialog.vue b/apps/desktop/src/components/diagram/SchemaDiagramDialog.vue index 10e16ac343..8094ffff8c 100644 --- a/apps/desktop/src/components/diagram/SchemaDiagramDialog.vue +++ b/apps/desktop/src/components/diagram/SchemaDiagramDialog.vue @@ -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[] = []; diff --git a/apps/desktop/src/components/diff/DataCompareDialog.vue b/apps/desktop/src/components/diff/DataCompareDialog.vue index d3a1063b43..b272674044 100644 --- a/apps/desktop/src/components/diff/DataCompareDialog.vue +++ b/apps/desktop/src/components/diff/DataCompareDialog.vue @@ -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)); diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index 62686e38bc..d5b33bed9e 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -394,7 +394,7 @@ async function ensureTableObjectGroupsLoaded(target: Extract): 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) { diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index be409c68e4..4e83abdb4e 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -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 = 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 = new Set(["connection-group", "database", "schema", "table", "view", "redis-db", "mongo-db", "mongo-collection", "elasticsearch-index"]); +const groupTypes: Set = 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 = 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[] { /> {{ visibleLabel(node) }} {{ node.objectCount }} diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index caa1a79630..e38e1dd366 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1220,6 +1220,7 @@ triggers: "Triggers", tables: "Tables", views: "Views", + materializedViews: "Materialized Views", procedures: "Procedures", functions: "Functions", sequences: "Sequences", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index dc2e51503c..cef8911f2c 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1219,6 +1219,7 @@ triggers: "触发器", tables: "表", views: "视图", + materializedViews: "物化视图", procedures: "存储过程", functions: "函数", sequences: "序列", diff --git a/apps/desktop/src/lib/databaseObjectCapabilities.ts b/apps/desktop/src/lib/databaseObjectCapabilities.ts index ee696f9f16..e3e6ce7747 100644 --- a/apps/desktop/src/lib/databaseObjectCapabilities.ts +++ b/apps/desktop/src/lib/databaseObjectCapabilities.ts @@ -1,6 +1,6 @@ import type { DatabaseType } from "@/types/database"; -export type SidebarObjectKind = "TABLE" | "VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY"; +export type SidebarObjectKind = "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY"; export interface DatabaseObjectCapabilities { sidebarObjects: SidebarObjectKind[]; @@ -13,8 +13,9 @@ const TABLE_VIEW_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW"]; const TABLE_VIEW_PROCEDURE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE"]; const TABLE_FUNCTION_OBJECTS: SidebarObjectKind[] = ["TABLE", "FUNCTION"]; const ROUTINE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION"]; -const POSTGRES_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE"]; -const ORACLE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE_BODY"]; +const POSTGRES_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE"]; +const POSTGRES_LIKE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION"]; +const ORACLE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE_BODY"]; const TABLE_ONLY_TYPES = new Set(["influxdb"]); const TABLE_FUNCTION_TYPES = new Set(["manticoresearch"]); @@ -23,6 +24,7 @@ const TABLE_VIEW_ONLY_TYPES = new Set(["sqlite", "rqlite", "turso" const ORACLE_PACKAGE_TYPES = new Set(["oracle", "oceanbase-oracle"]); const POSTGRES_SEQUENCE_TYPES = new Set(["postgres", "gaussdb", "kwdb", "opengauss"]); +const POSTGRES_LIKE_TYPES = new Set(["kingbase", "highgo", "vastbase", "redshift"]); export function databaseObjectCapabilities(dbType?: DatabaseType): DatabaseObjectCapabilities { const sidebarObjects = sidebarObjectKindsForDatabase(dbType); @@ -41,6 +43,7 @@ export function sidebarObjectKindsForDatabase(dbType?: DatabaseType): SidebarObj if (TABLE_VIEW_PROCEDURE_TYPES.has(dbType)) return [...TABLE_VIEW_PROCEDURE_OBJECTS]; if (TABLE_VIEW_ONLY_TYPES.has(dbType)) return [...TABLE_VIEW_OBJECTS]; if (POSTGRES_SEQUENCE_TYPES.has(dbType)) return [...POSTGRES_OBJECTS]; + if (POSTGRES_LIKE_TYPES.has(dbType)) return [...POSTGRES_LIKE_OBJECTS]; return [...ROUTINE_OBJECTS]; } @@ -48,6 +51,7 @@ export function normalizeSidebarObjectKind(type: string): SidebarObjectKind { const value = type.toUpperCase(); if (value.includes("PACKAGE BODY") || value.includes("PACKAGE_BODY")) return "PACKAGE_BODY"; if (value.includes("PACKAGE")) return "PACKAGE"; + if (value.includes("MATERIALIZED") && value.includes("VIEW")) return "MATERIALIZED_VIEW"; if (value.includes("VIEW")) return "VIEW"; if (value.includes("SEQ")) return "SEQUENCE"; if (value.includes("PROC")) return "PROCEDURE"; diff --git a/apps/desktop/src/lib/objectBrowserRows.ts b/apps/desktop/src/lib/objectBrowserRows.ts index 4ce9f4ee0c..4a7ab0dd99 100644 --- a/apps/desktop/src/lib/objectBrowserRows.ts +++ b/apps/desktop/src/lib/objectBrowserRows.ts @@ -6,7 +6,7 @@ export type ObjectBrowserRow = { id: string; name: string; schema?: string; - type: "TABLE" | "VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY"; + type: "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY"; comment?: string | null; created_at?: string | null; updated_at?: string | null; @@ -23,6 +23,7 @@ export function normalizeObjectBrowserType(type: string): ObjectBrowserRow["type const value = type.toUpperCase(); if (value.includes("PACKAGE BODY") || value.includes("PACKAGE_BODY")) return "PACKAGE_BODY"; if (value.includes("PACKAGE")) return "PACKAGE"; + if (value.includes("MATERIALIZED") && value.includes("VIEW")) return "MATERIALIZED_VIEW"; if (value.includes("VIEW")) return "VIEW"; if (value.includes("SEQ")) return "SEQUENCE"; if (value.includes("PROC")) return "PROCEDURE"; diff --git a/apps/desktop/src/lib/objectRenameSql.ts b/apps/desktop/src/lib/objectRenameSql.ts index 7559b8804b..37d4517c1e 100644 --- a/apps/desktop/src/lib/objectRenameSql.ts +++ b/apps/desktop/src/lib/objectRenameSql.ts @@ -23,8 +23,8 @@ export function supportsObjectRename(databaseType: DatabaseType | undefined, obj } if (databaseType === "sqlite" || databaseType === "rqlite" || databaseType === "turso") return objectType === "TABLE"; if (databaseType === "mysql" || databaseType === "goldendb") return objectType === "TABLE" || objectType === "VIEW"; - if (postgresLikeRenameTypes.has(databaseType)) return objectType === "TABLE" || objectType === "VIEW"; - if (oracleLikeRenameTypes.has(databaseType)) return objectType === "TABLE" || objectType === "VIEW"; + if (postgresLikeRenameTypes.has(databaseType)) return objectType === "TABLE" || objectType === "VIEW" || objectType === "MATERIALIZED_VIEW"; + if (oracleLikeRenameTypes.has(databaseType)) return objectType === "TABLE" || objectType === "VIEW" || objectType === "MATERIALIZED_VIEW"; return false; } diff --git a/apps/desktop/src/lib/sidebarActiveTabTarget.ts b/apps/desktop/src/lib/sidebarActiveTabTarget.ts index ceacaef78c..b40d16bb70 100644 --- a/apps/desktop/src/lib/sidebarActiveTabTarget.ts +++ b/apps/desktop/src/lib/sidebarActiveTabTarget.ts @@ -105,7 +105,7 @@ export function matchesTarget(node: TreeNode, target: ActiveTabSidebarTarget): b return node.type === "saved-sql-file" && node.savedSqlId === target.savedSqlId; } - return (node.type === "table" || node.type === "view") && node.connectionId === target.connectionId && node.database === target.database && schemaMatches(node, target.schema) && node.label === target.tableName; + return (node.type === "table" || node.type === "view" || node.type === "materialized_view") && node.connectionId === target.connectionId && node.database === target.database && schemaMatches(node, target.schema) && node.label === target.tableName; } export function findSidebarNodeForActiveTab(tab: QueryTab | undefined | null, flatNodes: readonly FlatTreeNode[]): FlatTreeNode | null { diff --git a/apps/desktop/src/lib/sidebarTreeItemLayout.ts b/apps/desktop/src/lib/sidebarTreeItemLayout.ts index 94e57c0b9e..4d758d7d5d 100644 --- a/apps/desktop/src/lib/sidebarTreeItemLayout.ts +++ b/apps/desktop/src/lib/sidebarTreeItemLayout.ts @@ -2,7 +2,7 @@ import type { TreeNodeType } from "@/types/database"; const leafTypes: Set = new Set(["column", "index", "fkey", "trigger", "procedure", "function", "package", "package-body", "object-browser", "redis-db", "mongo-collection", "elasticsearch-index", "user-admin", "saved-sql-file", "load-more"]); -const fullWidthLabelTypes: Set = new Set(["table", "view", "mongo-collection", "elasticsearch-index"]); +const fullWidthLabelTypes: Set = new Set(["table", "view", "materialized_view", "mongo-collection", "elasticsearch-index"]); const emptyContainerTypes: Set = new Set(["saved-sql-root", "saved-sql-folder"]); diff --git a/apps/desktop/src/lib/tableTree.ts b/apps/desktop/src/lib/tableTree.ts index eb517ab19d..087fcbf8d5 100644 --- a/apps/desktop/src/lib/tableTree.ts +++ b/apps/desktop/src/lib/tableTree.ts @@ -70,7 +70,7 @@ function makeTableTreeEntry({ const node: TreeNode = { id: `${nodeId}:${nodeIdSchemaSuffix}${name}`, label: name, - type: objectType === "VIEW" ? ("view" as const) : ("table" as const), + type: objectType === "VIEW" ? ("view" as const) : objectType === "MATERIALIZED_VIEW" ? ("materialized_view" as const) : ("table" as const), comment, connectionId, database, @@ -213,7 +213,7 @@ export function mergeTableInfosIntoObjects(objects: readonly ObjectInfo[], table for (const table of tables) { const objectType = normalizeObjectType(table.table_type); - if (objectType !== "TABLE" && objectType !== "VIEW") continue; + if (objectType !== "TABLE" && objectType !== "VIEW" && objectType !== "MATERIALIZED_VIEW") continue; const name = normalizeDatabaseObjectName(table.name); if (!name) continue; const matchingObject = objects.find((obj) => { @@ -307,7 +307,7 @@ export function hasTablePartitionGroups(node: TreeNode): boolean { export type DatabaseObjectTreeKind = SidebarObjectKind; -function buildObjectTreeEntries({ nodeId, connectionId, database, schema, objects, objectType }: { nodeId: string; connectionId: string; database: string; schema?: string; objects: ObjectInfo[]; objectType: "TABLE" | "VIEW" }): TreeNode[] { +function buildObjectTreeEntries({ nodeId, connectionId, database, schema, objects, objectType }: { nodeId: string; connectionId: string; database: string; schema?: string; objects: ObjectInfo[]; objectType: "TABLE" | "VIEW" | "MATERIALIZED_VIEW" }): TreeNode[] { const entries = objects.flatMap((obj) => { const name = normalizeDatabaseObjectName(obj.name); if (!name) return []; @@ -337,7 +337,7 @@ export function buildSimpleObjectTreeNodes({ nodeId, connectionId, database, sch for (const obj of objects) { const objectType = normalizeObjectType(obj.object_type); - if (!["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE", "PACKAGE", "PACKAGE_BODY"].includes(objectType)) { + if (!["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE", "PACKAGE", "PACKAGE_BODY"].includes(objectType)) { continue; } @@ -365,7 +365,7 @@ export function buildSimpleObjectTreeNodes({ nodeId, connectionId, database, sch } else { const simpleNodeType = simpleObjectNodeType(objectType); objectNodes.push({ - id: objectType === "VIEW" ? entry.node.id : `${nodeId}:${childSchema ? `${childSchema}:` : ""}${name}:${objectType}`, + id: objectType === "VIEW" || objectType === "MATERIALIZED_VIEW" ? entry.node.id : `${nodeId}:${childSchema ? `${childSchema}:` : ""}${name}:${objectType}`, label: name, type: simpleNodeType, comment: obj.comment, @@ -383,6 +383,7 @@ export function buildSimpleObjectTreeNodes({ nodeId, connectionId, database, sch function simpleObjectNodeType(objectType: DatabaseObjectTreeKind): TreeNodeType { if (objectType === "VIEW") return "view"; + if (objectType === "MATERIALIZED_VIEW") return "materialized_view"; if (objectType === "PROCEDURE") return "procedure"; if (objectType === "FUNCTION") return "function"; if (objectType === "SEQUENCE") return "sequence"; @@ -404,6 +405,13 @@ const groupDefs: Array<{ }> = [ { key: "__tables", label: "tree.tables", objectTypes: ["TABLE"], nodeType: "group-tables", childType: "table" }, { key: "__views", label: "tree.views", objectTypes: ["VIEW"], nodeType: "group-views", childType: "view" }, + { + key: "__materialized_views", + label: "tree.materializedViews", + objectTypes: ["MATERIALIZED_VIEW"], + nodeType: "group-materialized-views", + childType: "materialized_view", + }, { key: "__procedures", label: "tree.procedures", @@ -434,7 +442,7 @@ const groupDefs: Array<{ }, ]; -const objectGroupNodeTypes = new Set(["group-tables", "group-views", "group-procedures", "group-functions", "group-sequences", "group-packages"]); +const objectGroupNodeTypes = new Set(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages"]); export function buildObjectGroupPlaceholderNodes({ nodeId, connectionId, database, schema, objectTypes }: { nodeId: string; connectionId: string; database: string; schema?: string; objectTypes: DatabaseObjectTreeKind[] }): TreeNode[] { const supported = new Set(objectTypes); @@ -483,7 +491,7 @@ export function buildGroupedObjectTreeNodes({ nodeId, connectionId, database, sc for (const def of groupDefs) { const items = def.objectTypes.flatMap((objectType) => buckets.get(objectType) ?? []); if (!items?.length) continue; - const isExpandable = def.nodeType === "group-tables" || def.nodeType === "group-views"; + const isExpandable = def.nodeType === "group-tables" || def.nodeType === "group-views" || def.nodeType === "group-materialized-views"; const children = isExpandable ? buildObjectTreeEntries({ nodeId: `${nodeId}:${def.key}`, diff --git a/apps/desktop/src/lib/treeNodeClick.ts b/apps/desktop/src/lib/treeNodeClick.ts index eb39a59947..1f8644d22c 100644 --- a/apps/desktop/src/lib/treeNodeClick.ts +++ b/apps/desktop/src/lib/treeNodeClick.ts @@ -6,16 +6,17 @@ export type TreeNodeRowDoubleClickAction = "open-data" | "open-object-browser" | export type SidebarSelectionCopyAction = "copy-name" | "none"; export type SidebarActivation = "single" | "double"; -const dataNodeTypes = new Set(["table", "view"]); +const dataNodeTypes = new Set(["table", "view", "materialized_view"]); const toggleLeafNodeTypes = new Set(["redis-db", "mongo-collection", "elasticsearch-index", "user-admin"]); const objectBrowserNodeTypes = new Set(["database", "schema", "object-browser"]); -const sourceNodeTypes = new Set(["procedure", "function", "sequence", "package", "package-body"]); +const sourceNodeTypes = new Set(["materialized_view", "procedure", "function", "sequence", "package", "package-body"]); const savedSqlNodeTypes = new Set(["saved-sql-file"]); const tableChildGroupNodeTypes = new Set(["group-columns", "group-indexes", "group-fkeys", "group-triggers", "group-partitions"]); -const databaseChildGroupNodeTypes = new Set(["group-tables", "group-views", "group-procedures", "group-functions", "group-sequences", "group-packages"]); +const databaseChildGroupNodeTypes = new Set(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages"]); export function objectSourceKindForTreeNode(type: TreeNodeType): ObjectSourceKind | null { if (type === "view") return "VIEW"; + if (type === "materialized_view") return "MATERIALIZED_VIEW"; if (type === "procedure") return "PROCEDURE"; if (type === "function") return "FUNCTION"; if (type === "sequence") return "SEQUENCE"; diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 01d8f7cc83..cd7685891f 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -1275,7 +1275,7 @@ export const useConnectionStore = defineStore("connection", () => { } } - const wantsOnlyTablesOrViews = objectTypes.every((objectType) => objectType === "TABLE" || objectType === "VIEW"); + const wantsOnlyTablesOrViews = objectTypes.every((objectType) => objectType === "TABLE" || objectType === "VIEW" || objectType === "MATERIALIZED_VIEW"); let children: TreeNode[]; if (wantsOnlyTablesOrViews) { const page = await loadPagedTableGroupChildren({ @@ -1323,7 +1323,7 @@ export const useConnectionStore = defineStore("connection", () => { const objectTypes = objectTypesForGroupNode(parent.type); const parentNodeId = objectGroupRefreshParentId(parent); if (!objectTypes || !parentNodeId) return; - if (!objectTypes.every((objectType) => objectType === "TABLE" || objectType === "VIEW")) return; + if (!objectTypes.every((objectType) => objectType === "TABLE" || objectType === "VIEW" || objectType === "MATERIALIZED_VIEW")) return; const config = getConfig(parent.connectionId); const querySchema = connectionObjectTreeQuerySchema(config, parent.database, parent.schema); @@ -1605,7 +1605,7 @@ export const useConnectionStore = defineStore("connection", () => { } } else if (node.type === "schema" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.schema) { await loadTables(node.connectionId, node.database, node.schema, options); - } 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 loadTableGroups(node.connectionId, node.database, node.label, node.schema, node.id); } else if (node.type === "group-columns" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) { await loadColumns(node.connectionId, node.database, node.tableName, node.schema, node.id); @@ -1615,7 +1615,7 @@ export const useConnectionStore = defineStore("connection", () => { await loadForeignKeys(node.connectionId, node.database, node.tableName, node.schema, node.id); } else if (node.type === "group-triggers" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) { await loadTriggers(node.connectionId, node.database, node.tableName, node.schema, node.id); - } else 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") { + } else 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") { await loadObjectGroupChildren(node, options); } else if (node.type === "group-partitions") { node.isExpanded = true; @@ -1962,7 +1962,7 @@ export const useConnectionStore = defineStore("connection", () => { return tables.map((table) => ({ name: table.name, schema: s, - type: table.table_type === "VIEW" ? ("view" as const) : ("table" as const), + type: table.table_type === "VIEW" || table.table_type === "MATERIALIZED VIEW" ? ("view" as const) : ("table" as const), })) as SqlCompletionTable[]; } catch { return [] as SqlCompletionTable[]; @@ -1984,7 +1984,7 @@ export const useConnectionStore = defineStore("connection", () => { return tables.map((table) => ({ name: table.name, schema: s, - type: table.table_type === "VIEW" ? ("view" as const) : ("table" as const), + type: table.table_type === "VIEW" || table.table_type === "MATERIALIZED VIEW" ? ("view" as const) : ("table" as const), })) as SqlCompletionTable[]; } catch { return [] as SqlCompletionTable[]; @@ -2011,7 +2011,7 @@ export const useConnectionStore = defineStore("connection", () => { return tables.map((table) => ({ name: table.name, schema, - type: table.table_type === "VIEW" ? ("view" as const) : ("table" as const), + type: table.table_type === "VIEW" || table.table_type === "MATERIALIZED VIEW" ? ("view" as const) : ("table" as const), })); } catch { return []; @@ -2030,7 +2030,7 @@ export const useConnectionStore = defineStore("connection", () => { } completionTablesCache.value[cacheKey] = tables.map((table) => ({ name: table.name, - type: table.table_type === "VIEW" ? ("view" as const) : ("table" as const), + type: table.table_type === "VIEW" || table.table_type === "MATERIALIZED VIEW" ? ("view" as const) : ("table" as const), })); completionTablesCache.value[cacheKey] = limit ? completionTablesCache.value[cacheKey].slice(0, limit) : completionTablesCache.value[cacheKey]; indexCompletionTables(connectionId, database, schema, completionTablesCache.value[cacheKey]); diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index 43fba67443..9678cf2435 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -211,7 +211,7 @@ export interface TableInfo { parent_name?: string | null; } -export type DatabaseObjectType = "TABLE" | "VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY"; +export type DatabaseObjectType = "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY"; export interface ObjectInfo { name: string; @@ -224,7 +224,7 @@ export interface ObjectInfo { parent_name?: string | null; } -export type ObjectSourceKind = "VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY"; +export type ObjectSourceKind = "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY"; export interface ObjectSource { name: string; @@ -390,6 +390,7 @@ export type TreeNodeType = | "schema" | "table" | "view" + | "materialized_view" | "procedure" | "function" | "sequence" @@ -401,6 +402,7 @@ export type TreeNodeType = | "group-triggers" | "group-tables" | "group-views" + | "group-materialized-views" | "group-procedures" | "group-functions" | "group-sequences" diff --git a/crates/dbx-core/src/db_admin_sql.rs b/crates/dbx-core/src/db_admin_sql.rs index 3d2615c728..b6ae0401ed 100644 --- a/crates/dbx-core/src/db_admin_sql.rs +++ b/crates/dbx-core/src/db_admin_sql.rs @@ -8,6 +8,7 @@ use crate::sql_dialect::{is_schema_aware, qualified_table_name, quote_table_iden pub enum DatabaseObjectType { Table, View, + MaterializedView, Procedure, Function, } @@ -325,7 +326,10 @@ pub fn supports_object_rename(database_type: Option, object_type: return matches!(object_type, DatabaseObjectType::Table | DatabaseObjectType::View); } if is_postgres_like_rename(database_type) || is_oracle_like_rename(database_type) { - return matches!(object_type, DatabaseObjectType::Table | DatabaseObjectType::View); + return matches!( + object_type, + DatabaseObjectType::Table | DatabaseObjectType::View | DatabaseObjectType::MaterializedView + ); } false } @@ -462,6 +466,7 @@ fn object_type_keyword(object_type: DatabaseObjectType) -> &'static str { match object_type { DatabaseObjectType::Table => "TABLE", DatabaseObjectType::View => "VIEW", + DatabaseObjectType::MaterializedView => "MATERIALIZED VIEW", DatabaseObjectType::Procedure => "PROCEDURE", DatabaseObjectType::Function => "FUNCTION", }