From d59997c0909c6387b9e046c0baefb580cc16a212 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 19 Jun 2026 14:35:32 +0800 Subject: [PATCH] feat(objects): add table statistics columns --- .../src/components/objects/ObjectBrowser.vue | 78 ++++++++++++++++--- apps/desktop/src/i18n/locales/en.ts | 3 + apps/desktop/src/i18n/locales/es.ts | 3 + apps/desktop/src/i18n/locales/it.ts | 3 + apps/desktop/src/i18n/locales/ja.ts | 3 + apps/desktop/src/i18n/locales/pt-BR.ts | 3 + apps/desktop/src/i18n/locales/zh-CN.ts | 3 + apps/desktop/src/i18n/locales/zh-TW.ts | 3 + apps/desktop/src/lib/api.ts | 1 + apps/desktop/src/lib/http.ts | 5 ++ apps/desktop/src/lib/objectBrowserRows.ts | 48 +++++++++++- apps/desktop/src/lib/tauri.ts | 5 ++ apps/desktop/src/types/database.ts | 7 ++ crates/dbx-core/src/db/mysql.rs | 39 +++++++++- crates/dbx-core/src/db/postgres.rs | 31 +++++++- crates/dbx-core/src/db/sqlserver.rs | 41 +++++++++- crates/dbx-core/src/schema.rs | 37 +++++++++ crates/dbx-core/src/types.rs | 8 ++ crates/dbx-web/src/main.rs | 1 + crates/dbx-web/src/routes/schema.rs | 12 +++ packages/app-tests/objectBrowserRows.test.ts | 34 +++++++- src-tauri/src/commands/schema.rs | 10 +++ src-tauri/src/lib.rs | 1 + 23 files changed, 360 insertions(+), 19 deletions(-) diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index 119faacac2..b4c02fe760 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -44,7 +44,7 @@ import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomC import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue"; import ProcedureExecutionDialog from "@/components/objects/ProcedureExecutionDialog.vue"; import * as api from "@/lib/api"; -import type { ConnectionConfig, ForeignKeyInfo, ObjectInfo, ObjectSourceKind } from "@/types/database"; +import type { ConnectionConfig, ForeignKeyInfo, ObjectInfo, ObjectSourceKind, ObjectStatistics } from "@/types/database"; import { sortTablesByFkDependency, type TableWithFk } from "@/lib/tableDependencySort"; import { isSchemaAware } from "@/lib/databaseCapabilities"; import { supportsSchemaDiagram, supportsTableImport, supportsTableStructureEditing, supportsTableTruncate } from "@/lib/databaseFeatureSupport"; @@ -69,7 +69,18 @@ import DdlViewDialog from "./DdlViewDialog.vue"; import type { SqlFormatDialect } from "@/lib/sqlFormatter"; import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { buildObjectBrowserRows, filterObjectBrowserRows, formatObjectBrowserTimestamp, initialObjectBrowserSortDirection, sortObjectBrowserRows, type ObjectBrowserRow, type ObjectBrowserSortDirection, type ObjectBrowserSortKey } from "@/lib/objectBrowserRows"; +import { + buildObjectBrowserRows, + filterObjectBrowserRows, + formatObjectBrowserBytes, + formatObjectBrowserCount, + formatObjectBrowserTimestamp, + initialObjectBrowserSortDirection, + sortObjectBrowserRows, + type ObjectBrowserRow, + type ObjectBrowserSortDirection, + type ObjectBrowserSortKey, +} from "@/lib/objectBrowserRows"; type ObjectFilter = "all" | "tables" | "views" | "procedures" | "functions" | "sequences" | "packages"; @@ -189,14 +200,13 @@ const objectFilters = computed(() => .map(([filter]) => filter), ); const showObjectFilter = computed(() => objectFilters.value.length > 2); -const hasComments = computed(() => rows.value.some((row) => row.comment?.trim())); const hasCreatedAt = computed(() => rows.value.some((row) => row.created_at?.trim())); const hasUpdatedAt = computed(() => rows.value.some((row) => row.updated_at?.trim())); const gridTemplateColumns = computed(() => { - const columns = ["34px", "minmax(0,1fr)", "120px"]; + const columns = ["34px", "minmax(160px,1fr)", "110px", "110px", "100px"]; if (hasCreatedAt.value) columns.push("150px"); if (hasUpdatedAt.value) columns.push("150px"); - if (hasComments.value) columns.push("minmax(160px,0.7fr)"); + columns.push("minmax(160px,0.7fr)"); return columns.join(" "); }); const partitionRowsByParentId = computed(() => { @@ -1042,6 +1052,7 @@ async function loadObjects() { const availableTableIds = new Set(rows.value.filter((row) => row.type === "TABLE").map((row) => row.id)); setSelectedTableIds(new Set([...selectedTableIds.value].filter((id) => availableTableIds.has(id)))); expandedPartitionParentIds.value = new Set([...expandedPartitionParentIds.value].filter((id) => rows.value.some((row) => row.id === id && row.partitionCount))); + void loadObjectStatistics(id, schema); } catch (e: any) { if (id !== loadId) return; error.value = e?.message || String(e); @@ -1055,6 +1066,39 @@ async function loadObjects() { } } +async function loadObjectStatistics(id: number, schema: string) { + if (!rows.value.some((row) => row.type === "TABLE")) return; + try { + const stats = await api.listObjectStatistics(props.connection.id, props.database, schema); + if (id !== loadId || stats.length === 0) return; + mergeObjectStatistics(stats, schema); + } catch (e) { + console.debug("[ObjectBrowser] table statistics unavailable", e); + } +} + +function mergeObjectStatistics(stats: ObjectStatistics[], fallbackSchema: string) { + const statsByKey = new Map(stats.map((stat) => [objectStatisticKey(stat.schema || fallbackSchema, stat.name), stat])); + rows.value = rows.value.map((row) => { + if (row.type !== "TABLE") return row; + const stat = statsByKey.get(objectStatisticKey(row.schema || fallbackSchema, row.name)); + if (!stat) return row; + return { + ...row, + estimatedRows: normalizeStatisticNumber(stat.estimated_rows), + totalBytes: normalizeStatisticNumber(stat.total_bytes), + }; + }); +} + +function objectStatisticKey(schema: string | undefined, name: string) { + return `${schema || ""}\0${name}`.toLowerCase(); +} + +function normalizeStatisticNumber(value: number | null | undefined): number | null { + return typeof value === "number" && Number.isFinite(value) ? value : null; +} + async function reload() { await loadSchemas(); await loadObjects(); @@ -1310,7 +1354,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] { {{ t("objects.empty") }}
-
+
+ + -
- +