mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
feat(grid): add table info overview tab to the info drawer
This commit is contained in:
@@ -91,10 +91,11 @@ import TemporalCellEditor from "@/components/grid/TemporalCellEditor.vue";
|
||||
import EnumCellEditor from "@/components/grid/EnumCellEditor.vue";
|
||||
import DataGridReadonlyTextSelection from "@/components/grid/DataGridReadonlyTextSelection.vue";
|
||||
import GridSnapshotDialog from "@/components/grid/GridSnapshotDialog.vue";
|
||||
import type { QueryResult, ColumnInfo, ConstraintInfo, DatabaseType, ForeignKeyInfo, IndexInfo, PgTablePartitioning, TriggerInfo, TableInfoTab, QueryResultSourceColumnRef, QueryPageJumpProgress } from "@/types/database";
|
||||
import type { QueryResult, ColumnInfo, ConstraintInfo, DatabaseType, ForeignKeyInfo, IndexInfo, ObjectStatistics, PgTablePartitioning, TriggerInfo, TableInfoTab, QueryResultSourceColumnRef, QueryPageJumpProgress } from "@/types/database";
|
||||
import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
|
||||
import { shouldNavigateFromTableInfoColumnClick } from "@/lib/table/tableInfoColumnNavigation";
|
||||
import { tableInfoTabForDrawerToggle } from "@/lib/table/tableInfoTabPreference";
|
||||
import { findTableStatistics } from "@/lib/dataGrid/tableInfoOverview";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { formatElapsedSeconds } from "@/lib/common/elapsedTime";
|
||||
import type { SqlInsertMode } from "@/lib/export/sqlInsertMode";
|
||||
@@ -10346,6 +10347,13 @@ const tableOwner = ref<string | null>(null);
|
||||
const tableOwnerLoading = ref(false);
|
||||
const tableOwnerError = ref("");
|
||||
const tableOwnerRequestGeneration = ref(0);
|
||||
// Overview tab state: row/size statistics plus the table comment, loaded lazily
|
||||
// when the tab is first selected.
|
||||
const tableOverviewStats = ref<ObjectStatistics | null>(null);
|
||||
const tableOverviewComment = ref<string | null>(null);
|
||||
const tableOverviewLoading = ref(false);
|
||||
const tableOverviewLoaded = ref(false);
|
||||
const tableOverviewRequestGeneration = ref(0);
|
||||
const canShowTableOwner = computed(() => resolvedDatabaseType.value === "postgres" && !!props.connectionId && !!props.database && !!props.tableMeta?.schema && !!props.tableMeta?.tableName);
|
||||
|
||||
function scrollDdlSearchMatchIntoView(match: HTMLElement) {
|
||||
@@ -10455,6 +10463,7 @@ const partitionStatusResolved = ref(false);
|
||||
const constraintsForTab = computed(() => constraintsForConstraintsTab(constraints.value, tableMetadataCapabilities.value.foreignKeys));
|
||||
const searchQuery = ref("");
|
||||
const activeTableInfoLoading = computed(() => {
|
||||
if (activeTableInfoTab.value === "info") return tableOverviewLoading.value;
|
||||
if (activeTableInfoTab.value === "ddl") return ddlLoading.value;
|
||||
if (activeTableInfoTab.value === "columns") return tableInfoColumnsLoading.value;
|
||||
if (activeTableInfoTab.value === "indexes") return indexesLoading.value;
|
||||
@@ -10635,9 +10644,41 @@ const metadataLoaders = useDataGridTableMetadataLoaders({
|
||||
});
|
||||
|
||||
const { fetchDdl, fetchTableInfoColumns, fetchTableOwner, currentIndexTableIdentity, fetchIndexes, refreshMongoIndexMetadataAfterMutation, currentForeignKeyTableIdentity, fetchForeignKeys: fetchForeignKeysMetadata, fetchTriggers, fetchConstraints, fetchPartitions } = metadataLoaders;
|
||||
|
||||
async function fetchTableOverview(force = false) {
|
||||
const connectionId = props.connectionId;
|
||||
const database = props.database;
|
||||
const schema = props.tableMeta?.schema;
|
||||
const tableName = props.tableMeta?.tableName;
|
||||
if (!connectionId || !database || !tableName) return;
|
||||
if (!force && tableOverviewLoaded.value) return;
|
||||
const generation = ++tableOverviewRequestGeneration.value;
|
||||
tableOverviewLoading.value = true;
|
||||
try {
|
||||
// Both lookups are best-effort: drivers without statistics support simply
|
||||
// leave the corresponding rows hidden in the overview tab.
|
||||
const [stats, comment] = await Promise.all([
|
||||
api.listObjectStatistics(connectionId, database, schema ?? "").catch((error) => {
|
||||
console.debug("table overview statistics unavailable", error);
|
||||
return [] as ObjectStatistics[];
|
||||
}),
|
||||
api.getTableComment(connectionId, database, schema ?? "", tableName, props.tableMeta?.catalog).catch((error) => {
|
||||
console.debug("table overview comment unavailable", error);
|
||||
return null;
|
||||
}),
|
||||
]);
|
||||
if (generation !== tableOverviewRequestGeneration.value) return;
|
||||
tableOverviewStats.value = findTableStatistics(stats, tableName, schema) ?? null;
|
||||
tableOverviewComment.value = comment || null;
|
||||
tableOverviewLoaded.value = true;
|
||||
} finally {
|
||||
if (generation === tableOverviewRequestGeneration.value) tableOverviewLoading.value = false;
|
||||
}
|
||||
}
|
||||
tableMetadataLoaderFetchForeignKeys = fetchForeignKeysMetadata;
|
||||
const tableInfoTabs = computed(() => {
|
||||
const tabs: TableInfoTabItem[] = [];
|
||||
tabs.push({ id: "info", label: t("grid.tableInfoOverview"), icon: Info });
|
||||
if (tableMetadataCapabilities.value.ddl) {
|
||||
tabs.push({ id: "ddl", label: "DDL", icon: Code2 });
|
||||
}
|
||||
@@ -10721,7 +10762,8 @@ async function selectTableInfoTab(tab: TableInfoTab) {
|
||||
if (!nextTab) return;
|
||||
activeTableInfoTab.value = nextTab;
|
||||
if (tabSupported) settingsStore.updateEditorSettings({ tableInfoActiveTab: tab });
|
||||
if (nextTab === "ddl") await fetchDdl();
|
||||
if (nextTab === "info") await fetchTableOverview();
|
||||
else if (nextTab === "ddl") await fetchDdl();
|
||||
else if (nextTab === "indexes") await fetchIndexes();
|
||||
else if (nextTab === "foreignKeys") await fetchForeignKeys();
|
||||
else if (nextTab === "constraints") await fetchConstraints();
|
||||
@@ -10751,7 +10793,10 @@ async function refreshActiveTableInfo() {
|
||||
if (!showTableInfo.value || !props.tableMeta) return;
|
||||
if (canShowTableOwner.value) void fetchTableOwner(true);
|
||||
|
||||
if (activeTableInfoTab.value === "ddl") await fetchDdl(true);
|
||||
if (activeTableInfoTab.value === "info") {
|
||||
tableOverviewLoaded.value = false;
|
||||
await fetchTableOverview();
|
||||
} else if (activeTableInfoTab.value === "ddl") await fetchDdl(true);
|
||||
else if (activeTableInfoTab.value === "columns") await fetchTableInfoColumns(true);
|
||||
else if (activeTableInfoTab.value === "indexes") {
|
||||
indexesLoaded.value = false;
|
||||
@@ -10781,6 +10826,11 @@ watch(
|
||||
tableOwnerLoading.value = false;
|
||||
tableOwnerError.value = "";
|
||||
tableOwnerRequestGeneration.value += 1;
|
||||
tableOverviewStats.value = null;
|
||||
tableOverviewComment.value = null;
|
||||
tableOverviewLoading.value = false;
|
||||
tableOverviewLoaded.value = false;
|
||||
tableOverviewRequestGeneration.value += 1;
|
||||
rawDdlContent.value = "";
|
||||
indexes.value = [];
|
||||
indexesLoaded.value = false;
|
||||
@@ -13525,6 +13575,13 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
|
||||
v-if="activeTableInfoTab !== 'ddl'"
|
||||
:active-tab="activeTableInfoTab"
|
||||
:search-query="searchQuery"
|
||||
:table-name="props.tableMeta?.tableName ?? ''"
|
||||
:table-schema="props.tableMeta?.schema ?? ''"
|
||||
:database="props.database ?? ''"
|
||||
:table-owner="tableOwner"
|
||||
:overview-stats="tableOverviewStats"
|
||||
:overview-comment="tableOverviewComment"
|
||||
:overview-loading="tableOverviewLoading"
|
||||
:columns="filteredColumns"
|
||||
:columns-loading="tableInfoColumnsLoading"
|
||||
:indexes="filteredIndexes"
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { KeyRound, Loader2, Trash2 } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import TablePartitionsPanel from "@/components/structure/TablePartitionsPanel.vue";
|
||||
import { tableColumnDefaultDisplayValue } from "@/lib/table/tableColumnDefaultPresentation";
|
||||
import type { ColumnInfo, ConstraintInfo, ForeignKeyInfo, IndexInfo, PgTablePartitioning, TableInfoTab, TriggerInfo } from "@/types/database";
|
||||
import { formatObjectBrowserBytes, formatObjectBrowserCount } from "@/lib/table/objectBrowserRows";
|
||||
import type { ColumnInfo, ConstraintInfo, ForeignKeyInfo, IndexInfo, ObjectStatistics, PgTablePartitioning, TableInfoTab, TriggerInfo } from "@/types/database";
|
||||
|
||||
interface DataGridTableInfoPanelsProps {
|
||||
activeTab: TableInfoTab;
|
||||
searchQuery: string;
|
||||
tableName: string;
|
||||
tableSchema: string;
|
||||
database: string;
|
||||
tableOwner: string | null;
|
||||
overviewStats: ObjectStatistics | null;
|
||||
overviewComment: string | null;
|
||||
overviewLoading: boolean;
|
||||
columns: ColumnInfo[];
|
||||
columnsLoading: boolean;
|
||||
indexes: IndexInfo[];
|
||||
@@ -40,6 +49,20 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const overviewRows = computed(() => {
|
||||
const rows: { label: string; value: string; mono?: boolean }[] = [
|
||||
{ label: t("common.table"), value: props.tableName },
|
||||
{ label: t("common.schema"), value: props.tableSchema },
|
||||
{ label: t("common.database"), value: props.database },
|
||||
{ label: t("grid.tableOwner"), value: props.tableOwner ?? "", mono: true },
|
||||
{ label: t("structureEditor.comment"), value: props.overviewComment ?? "" },
|
||||
{ label: t("grid.tableInfoEstimatedRows"), value: formatObjectBrowserCount(props.overviewStats?.estimated_rows), mono: true },
|
||||
{ label: t("grid.tableInfoTotalSize"), value: formatObjectBrowserBytes(props.overviewStats?.total_bytes), mono: true },
|
||||
];
|
||||
const query = props.searchQuery.trim().toLowerCase();
|
||||
return rows.filter((row) => row.value && (!query || row.label.toLowerCase().includes(query) || row.value.toLowerCase().includes(query)));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -218,6 +241,20 @@ const { t } = useI18n();
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="props.activeTab === 'info'" class="table-info-scroller flex-1 min-h-0 overflow-auto">
|
||||
<div v-if="props.overviewLoading" class="h-full flex items-center justify-center">
|
||||
<Loader2 class="w-4 h-4 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
<div v-else-if="props.searchQuery && overviewRows.length === 0" class="p-6 text-center text-xs text-muted-foreground">
|
||||
{{ t("grid.tableInfoNoResults") }}
|
||||
</div>
|
||||
<div v-else class="divide-y">
|
||||
<div v-for="row in overviewRows" :key="row.label" class="flex items-baseline gap-3 px-3 py-2 text-xs">
|
||||
<span class="w-20 shrink-0 text-muted-foreground">{{ row.label }}</span>
|
||||
<span class="min-w-0 flex-1 select-text break-words" :class="row.mono ? 'font-mono text-[11px]' : ''" :title="row.value">{{ row.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TablePartitionsPanel v-else-if="props.activeTab === 'partitions'" :partitioning="props.partitioning" :loading="props.partitionsLoading" :error="props.partitionsError" :search-query="props.searchQuery" />
|
||||
</template>
|
||||
|
||||
@@ -2328,6 +2328,9 @@ export default withEnglishFallback({
|
||||
rowsPerPageShort: " sətir",
|
||||
columnDetails: "Sütunun təfərrüatları",
|
||||
tableInfo: "Cədvəl haqqında məlumat",
|
||||
tableInfoOverview: "Ümumi baxış",
|
||||
tableInfoEstimatedRows: "Təxmini sətirlər",
|
||||
tableInfoTotalSize: "Ümumi həcm",
|
||||
pinTableInfo: "Cədvəl məlumatları panelini bərkit",
|
||||
unpinTableInfo: "Cədvəl məlumatları panelinin bərkidilməsini ləğv et",
|
||||
tableInfoColumns: "Sütunlar",
|
||||
@@ -2520,6 +2523,7 @@ export default withEnglishFallback({
|
||||
database: "Verilənlər bazası",
|
||||
table: "Cədvəl",
|
||||
view: "Görünüş",
|
||||
schema: "Sxem",
|
||||
materializedView: "Materiallaşdırılmış görünüş",
|
||||
procedure: "Prosedur",
|
||||
function: "Funksiya",
|
||||
|
||||
@@ -2522,6 +2522,9 @@ export default {
|
||||
rowsPerPageShort: " rows",
|
||||
columnDetails: "Column Details",
|
||||
tableInfo: "Table Info",
|
||||
tableInfoOverview: "Overview",
|
||||
tableInfoEstimatedRows: "Estimated rows",
|
||||
tableInfoTotalSize: "Total size",
|
||||
pinTableInfo: "Pin table info panel",
|
||||
unpinTableInfo: "Unpin table info panel",
|
||||
tableInfoColumns: "Columns",
|
||||
@@ -2714,6 +2717,7 @@ export default {
|
||||
database: "Database",
|
||||
table: "Table",
|
||||
view: "View",
|
||||
schema: "Schema",
|
||||
materializedView: "Materialized View",
|
||||
procedure: "Procedure",
|
||||
function: "Function",
|
||||
|
||||
@@ -2363,6 +2363,9 @@ export default withEnglishFallback({
|
||||
searchColumn: "Buscar columna/comentario...",
|
||||
noColumnsFound: "No se encontraron columnas",
|
||||
tableInfo: "Información de tabla",
|
||||
tableInfoOverview: "Resumen",
|
||||
tableInfoEstimatedRows: "Filas estimadas",
|
||||
tableInfoTotalSize: "Tamaño total",
|
||||
pinTableInfo: "Fijar el panel de información de la tabla",
|
||||
unpinTableInfo: "Desfijar el panel de información de la tabla",
|
||||
tableInfoColumns: "Columnas",
|
||||
@@ -2614,6 +2617,7 @@ export default withEnglishFallback({
|
||||
database: "Base de datos",
|
||||
table: "Tabla",
|
||||
view: "Vista",
|
||||
schema: "Esquema",
|
||||
materializedView: "Vista Materializada",
|
||||
procedure: "Procedimiento",
|
||||
function: "Función",
|
||||
|
||||
@@ -2349,6 +2349,9 @@ export default withEnglishFallback({
|
||||
rowsPerPageShort: " righe",
|
||||
columnDetails: "Dettagli Colonna",
|
||||
tableInfo: "Informazioni Tabella",
|
||||
tableInfoOverview: "Panoramica",
|
||||
tableInfoEstimatedRows: "Righe stimate",
|
||||
tableInfoTotalSize: "Dimensione totale",
|
||||
pinTableInfo: "Blocca il pannello delle informazioni sulla tabella",
|
||||
unpinTableInfo: "Sblocca il pannello delle informazioni sulla tabella",
|
||||
tableInfoColumns: "Colonne",
|
||||
@@ -2611,6 +2614,7 @@ export default withEnglishFallback({
|
||||
database: "Database",
|
||||
table: "Tabella",
|
||||
view: "Vista",
|
||||
schema: "Schema",
|
||||
materializedView: "Vista Materializzata",
|
||||
procedure: "Procedura",
|
||||
function: "Funzione",
|
||||
|
||||
@@ -2360,6 +2360,9 @@ export default withEnglishFallback({
|
||||
rowsPerPageShort: " 行",
|
||||
columnDetails: "列詳細",
|
||||
tableInfo: "テーブル情報",
|
||||
tableInfoOverview: "概要",
|
||||
tableInfoEstimatedRows: "推定行数",
|
||||
tableInfoTotalSize: "合計サイズ",
|
||||
pinTableInfo: "テーブル情報パネルを固定",
|
||||
unpinTableInfo: "テーブル情報パネルの固定を解除",
|
||||
tableInfoColumns: "列",
|
||||
@@ -2634,6 +2637,7 @@ export default withEnglishFallback({
|
||||
database: "データベース",
|
||||
table: "テーブル",
|
||||
view: "ビュー",
|
||||
schema: "スキーマ",
|
||||
materializedView: "マテリアライズドビュー",
|
||||
procedure: "プロシージャ",
|
||||
function: "関数",
|
||||
|
||||
@@ -2343,6 +2343,9 @@ export default withEnglishFallback({
|
||||
rowsPerPageShort: "행",
|
||||
columnDetails: "컬럼 상세",
|
||||
tableInfo: "테이블 정보",
|
||||
tableInfoOverview: "개요",
|
||||
tableInfoEstimatedRows: "예상 행 수",
|
||||
tableInfoTotalSize: "전체 크기",
|
||||
pinTableInfo: "테이블 정보 패널 고정",
|
||||
unpinTableInfo: "테이블 정보 패널 고정 해제",
|
||||
tableInfoColumns: "컬럼",
|
||||
@@ -2520,6 +2523,7 @@ export default withEnglishFallback({
|
||||
database: "데이터베이스",
|
||||
table: "테이블",
|
||||
view: "뷰",
|
||||
schema: "스키마",
|
||||
materializedView: "구체화된 뷰",
|
||||
procedure: "프로시저",
|
||||
function: "함수",
|
||||
|
||||
@@ -2351,6 +2351,9 @@ export default withEnglishFallback({
|
||||
rowsPerPageShort: " linhas",
|
||||
columnDetails: "Detalhes da Coluna",
|
||||
tableInfo: "Informações da Tabela",
|
||||
tableInfoOverview: "Visão geral",
|
||||
tableInfoEstimatedRows: "Linhas estimadas",
|
||||
tableInfoTotalSize: "Tamanho total",
|
||||
pinTableInfo: "Fixar painel de informações da tabela",
|
||||
unpinTableInfo: "Desafixar painel de informações da tabela",
|
||||
tableInfoColumns: "Colunas",
|
||||
@@ -2613,6 +2616,7 @@ export default withEnglishFallback({
|
||||
database: "Banco de dados",
|
||||
table: "Tabela",
|
||||
view: "Visualização",
|
||||
schema: "Esquema",
|
||||
materializedView: "Visualização Materializada",
|
||||
procedure: "Procedimento",
|
||||
function: "Função",
|
||||
|
||||
@@ -2323,6 +2323,9 @@ export default withEnglishFallback({
|
||||
rowsPerPageShort: " satır",
|
||||
columnDetails: "Sütun Ayrıntıları",
|
||||
tableInfo: "Tablo Bilgisi",
|
||||
tableInfoOverview: "Genel bakış",
|
||||
tableInfoEstimatedRows: "Tahmini satır",
|
||||
tableInfoTotalSize: "Toplam boyut",
|
||||
pinTableInfo: "Tablo bilgileri panelini sabitle",
|
||||
unpinTableInfo: "Tablo bilgileri panelinin sabitlemesini kaldır",
|
||||
tableInfoColumns: "Sütunlar",
|
||||
@@ -2498,6 +2501,7 @@ export default withEnglishFallback({
|
||||
database: "Veritabanı",
|
||||
table: "Tablo",
|
||||
view: "Görünüm",
|
||||
schema: "Şema",
|
||||
materializedView: "Materyalleştirilmiş Görünüm",
|
||||
procedure: "Yordam",
|
||||
function: "İşlev",
|
||||
|
||||
@@ -2435,6 +2435,9 @@ export default withEnglishFallback({
|
||||
rowsPerPageShort: " 行/页",
|
||||
columnDetails: "列详情",
|
||||
tableInfo: "表属性",
|
||||
tableInfoOverview: "表信息",
|
||||
tableInfoEstimatedRows: "估算行数",
|
||||
tableInfoTotalSize: "总大小",
|
||||
pinTableInfo: "固定表属性面板",
|
||||
unpinTableInfo: "取消固定表属性面板",
|
||||
tableInfoColumns: "字段",
|
||||
@@ -2626,6 +2629,7 @@ export default withEnglishFallback({
|
||||
database: "数据库",
|
||||
table: "表",
|
||||
view: "视图",
|
||||
schema: "Schema",
|
||||
materializedView: "物化视图",
|
||||
procedure: "存储过程",
|
||||
function: "函数",
|
||||
|
||||
@@ -2354,6 +2354,9 @@ export default withEnglishFallback({
|
||||
rowsPerPageShort: " 筆/頁",
|
||||
columnDetails: "欄位詳情",
|
||||
tableInfo: "資料表屬性",
|
||||
tableInfoOverview: "資料表資訊",
|
||||
tableInfoEstimatedRows: "預估列數",
|
||||
tableInfoTotalSize: "總大小",
|
||||
pinTableInfo: "固定資料表屬性面板",
|
||||
unpinTableInfo: "取消固定資料表屬性面板",
|
||||
tableInfoColumns: "欄位",
|
||||
@@ -2611,6 +2614,7 @@ export default withEnglishFallback({
|
||||
database: "資料庫",
|
||||
table: "資料表",
|
||||
view: "檢視",
|
||||
schema: "Schema",
|
||||
materializedView: "物化檢視",
|
||||
procedure: "預存程序",
|
||||
function: "函數",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { findTableStatistics } from "@/lib/dataGrid/tableInfoOverview";
|
||||
import type { ObjectStatistics } from "@/types/database";
|
||||
|
||||
const stats: ObjectStatistics[] = [
|
||||
{ name: "orders", schema: "public", estimated_rows: 100, total_bytes: 4096 },
|
||||
{ name: "orders", schema: "reporting", estimated_rows: 5, total_bytes: 1024 },
|
||||
{ name: "users", schema: null, estimated_rows: 42, total_bytes: 2048 },
|
||||
];
|
||||
|
||||
describe("findTableStatistics", () => {
|
||||
it("prefers an exact schema match", () => {
|
||||
expect(findTableStatistics(stats, "orders", "reporting")?.estimated_rows).toBe(5);
|
||||
expect(findTableStatistics(stats, "orders", "public")?.estimated_rows).toBe(100);
|
||||
});
|
||||
|
||||
it("falls back to a name-only match when the schema differs", () => {
|
||||
expect(findTableStatistics(stats, "users", "public")?.estimated_rows).toBe(42);
|
||||
});
|
||||
|
||||
it("matches by name when no schema is given", () => {
|
||||
expect(findTableStatistics(stats, "orders")?.estimated_rows).toBe(100);
|
||||
});
|
||||
|
||||
it("matches case-insensitively when drivers report inconsistent casing", () => {
|
||||
const mixed: ObjectStatistics[] = [
|
||||
{ name: "Orders", schema: "PUBLIC", estimated_rows: 7, total_bytes: 512 },
|
||||
{ name: "Orders", schema: "reporting", estimated_rows: 9, total_bytes: 256 },
|
||||
];
|
||||
expect(findTableStatistics(mixed, "orders", "public")?.estimated_rows).toBe(7);
|
||||
expect(findTableStatistics(mixed, "ORDERS", "REPORTING")?.estimated_rows).toBe(9);
|
||||
expect(findTableStatistics(mixed, "orders")?.estimated_rows).toBe(7);
|
||||
});
|
||||
|
||||
it("returns undefined when the table has no statistics", () => {
|
||||
expect(findTableStatistics(stats, "missing", "public")).toBeUndefined();
|
||||
expect(findTableStatistics([], "orders")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { ObjectStatistics } from "@/types/database";
|
||||
|
||||
/**
|
||||
* Picks the statistics entry for a table from a schema-level list. Drivers can
|
||||
* report inconsistent casing between `listObjects` and `listObjectStatistics`,
|
||||
* so both the schema and the name fold to lowercase — the same normalization
|
||||
* the object browser's statistic keys apply. The name-only fallback stays:
|
||||
* the statistics query is already scoped to one database/schema, and
|
||||
* MySQL-family tables carry an empty `tableMeta.schema` while their statistics
|
||||
* entries report the database as schema.
|
||||
*/
|
||||
export function findTableStatistics(stats: readonly ObjectStatistics[], tableName: string, schema?: string): ObjectStatistics | undefined {
|
||||
if (schema !== undefined && schema !== "") {
|
||||
const exact = stats.find((entry) => (entry.schema ?? "").toLowerCase() === schema.toLowerCase() && entry.name.toLowerCase() === tableName.toLowerCase());
|
||||
if (exact) return exact;
|
||||
}
|
||||
return stats.find((entry) => entry.name.toLowerCase() === tableName.toLowerCase());
|
||||
}
|
||||
@@ -27,6 +27,9 @@ export function visibleTableStructureRefreshScope(activeTab: TableInfoTab): Tabl
|
||||
return { columns: false, indexes: false, foreignKeys: false, constraints: false, triggers: false, partitions: true, tableComment: false };
|
||||
case "ddl":
|
||||
return { columns: false, indexes: false, foreignKeys: false, constraints: false, triggers: false, partitions: false, tableComment: true };
|
||||
default:
|
||||
// "info" is a data-grid drawer tab and has no structure-editor scope.
|
||||
return { columns: false, indexes: false, foreignKeys: false, constraints: false, triggers: false, partitions: false, tableComment: false };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1498,7 +1498,7 @@ function normalizeToolbarItems(items: Partial<ToolbarItems> | undefined): Toolba
|
||||
};
|
||||
}
|
||||
|
||||
const TABLE_INFO_TABS = new Set<TableInfoTab>(["ddl", "columns", "indexes", "foreignKeys", "constraints", "triggers", "partitions"]);
|
||||
const TABLE_INFO_TABS = new Set<TableInfoTab>(["info", "ddl", "columns", "indexes", "foreignKeys", "constraints", "triggers", "partitions"]);
|
||||
|
||||
function normalizeTableInfoTab(value: unknown): TableInfoTab {
|
||||
return typeof value === "string" && TABLE_INFO_TABS.has(value as TableInfoTab) ? (value as TableInfoTab) : DEFAULT_EDITOR_SETTINGS.tableInfoActiveTab;
|
||||
|
||||
@@ -1525,7 +1525,7 @@ export interface TableNameFilter {
|
||||
excludePatterns: string[];
|
||||
}
|
||||
|
||||
export type TableInfoTab = "columns" | "indexes" | "foreignKeys" | "constraints" | "triggers" | "partitions" | "ddl";
|
||||
export type TableInfoTab = "info" | "columns" | "indexes" | "foreignKeys" | "constraints" | "triggers" | "partitions" | "ddl";
|
||||
|
||||
/** PostgreSQL declarative partitioning strategy (`pg_partitioned_table.partstrat`). */
|
||||
export type PgPartitionKind = "range" | "list" | "hash";
|
||||
|
||||
Reference in New Issue
Block a user