feat(pg,kingbase): partition table operations and detail queries

This commit is contained in:
amwps290
2026-09-21 15:36:49 +08:00
committed by GitHub
parent 882bd130ab
commit 1000aa07b6
62 changed files with 6180 additions and 133 deletions
+71 -1
View File
@@ -1,6 +1,6 @@
import { strict as assert } from "node:assert";
import { test } from "vitest";
import { buildObjectBrowserRows, filterObjectBrowserRows, formatObjectBrowserBytes, formatObjectBrowserCount, formatObjectBrowserTimestamp, sortObjectBrowserRows, summarizeObjectBrowserSearch } from "../../apps/desktop/src/lib/table/objectBrowserRows.ts";
import { buildObjectBrowserRows, filterObjectBrowserRows, formatObjectBrowserBytes, formatObjectBrowserCount, formatObjectBrowserTimestamp, groupObjectBrowserRows, sortObjectBrowserRows, summarizeObjectBrowserSearch } from "../../apps/desktop/src/lib/table/objectBrowserRows.ts";
test("builds unique row ids for overloaded routines with the same visible name", () => {
const rows = buildObjectBrowserRows({
@@ -386,3 +386,73 @@ test("object browser timestamp display strips timezone suffixes", () => {
assert.equal(formatObjectBrowserTimestamp("2026-05-22 10:18:24+08"), "2026-05-22 10:18:24");
assert.equal(formatObjectBrowserTimestamp("2026-05-22 10:18:24.123456+08:00"), "2026-05-22 10:18:24");
});
function nestedPartitionRows() {
return buildObjectBrowserRows({
objects: [
{ name: "catalog_nested", object_type: "TABLE" },
{ name: "catalog_nested_2024", object_type: "TABLE", parent_schema: "public", parent_name: "catalog_nested" },
{ name: "catalog_nested_2025", object_type: "TABLE", parent_schema: "public", parent_name: "catalog_nested" },
{ name: "catalog_nested_2024_asia", object_type: "TABLE", parent_schema: "public", parent_name: "catalog_nested_2024" },
{ name: "catalog_nested_2024_eu", object_type: "TABLE", parent_schema: "public", parent_name: "catalog_nested_2024" },
{ name: "plain", object_type: "TABLE" },
],
database: "test",
fallbackSchema: "public",
needsSchema: false,
});
}
function group(rows, expandedIds, query = "", matchingRows = rows) {
return groupObjectBrowserRows({
rows,
matchingRows,
query,
expandedPartitionParentIds: new Set(expandedIds),
sortRows: (items) => sortObjectBrowserRows(items, "name", "asc"),
});
}
test("recurses through second-level sub-partitions instead of dropping them", () => {
const rows = nestedPartitionRows();
const root = rows.find((row) => row.name === "catalog_nested");
const year = rows.find((row) => row.name === "catalog_nested_2024");
assert.ok(root && year);
const result = group(rows, [root.id, year.id]);
assert.deepEqual(
result.rows.map((row) => row.name),
["catalog_nested", "catalog_nested_2024", "catalog_nested_2024_asia", "catalog_nested_2024_eu", "catalog_nested_2025", "plain"],
);
// Indentation depth drives the row indent in the UI.
assert.deepEqual(
result.rows.map((row) => result.depths.get(row.id)),
[0, 1, 2, 2, 1, 0],
);
});
test("only shows partition levels whose parent is expanded", () => {
const rows = nestedPartitionRows();
const root = rows.find((row) => row.name === "catalog_nested");
assert.ok(root);
assert.deepEqual(group(rows, []).rows.map((row) => row.name), ["catalog_nested", "plain"]);
assert.deepEqual(
group(rows, [root.id]).rows.map((row) => row.name),
["catalog_nested", "catalog_nested_2024", "catalog_nested_2025", "plain"],
);
});
test("a deep search match stays reachable while collapsed", () => {
const rows = nestedPartitionRows();
const match = rows.filter((row) => row.name === "catalog_nested_2024_asia");
assert.equal(match.length, 1);
// Collapsed (no expanded ids) and a query: the match must be shown under its
// ancestor chain, not dropped as an orphaned third-level partition.
const result = group(rows, [], "asia", match);
assert.deepEqual(result.rows.map((row) => row.name), ["catalog_nested", "catalog_nested_2024", "catalog_nested_2024_asia"]);
assert.deepEqual(result.rows.map((row) => result.depths.get(row.id)), [0, 1, 2]);
});
+3
View File
@@ -705,6 +705,9 @@ test("normalizes grid drawer widths", () => {
assert.equal(normalizeEditorSettings({ cellDetailDrawerWidth: 200 } as any).cellDetailDrawerWidth, 260);
assert.equal(normalizeEditorSettings({ tableInfoDrawerWidth: 1000 } as any).tableInfoDrawerWidth, 900);
assert.equal(normalizeEditorSettings({ tableInfoActiveTab: "columns" } as any).tableInfoActiveTab, "columns");
// The Partitions tab is a first-class table-info tab, so a saved preference
// for it must survive normalization instead of falling back to DDL.
assert.equal(normalizeEditorSettings({ tableInfoActiveTab: "partitions" } as any).tableInfoActiveTab, "partitions");
assert.equal(normalizeEditorSettings({ tableInfoActiveTab: "invalid" } as any).tableInfoActiveTab, "ddl");
assert.equal(normalizeEditorSettings({ cellDetailDrawerWidth: 456.7 } as any).cellDetailDrawerWidth, 457);
assert.equal(normalizeEditorSettings({ cellDetailPanelLayout: "right" } as any).cellDetailPanelLayout, "right");
+58
View File
@@ -0,0 +1,58 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { test } from "vitest";
// The table-properties drawer exists twice (DataGrid and ObjectBrowser), each
// with its own tab list, plus a shared body component. A partitions tab must be
// wired into all of them together, otherwise the drawer silently loses the tab
// on one surface.
const drawerSurfaces = [
"apps/desktop/src/components/grid/DataGrid.vue",
"apps/desktop/src/components/objects/ObjectBrowser.vue",
];
test("both table-properties drawers expose a capability-gated partitions tab", () => {
for (const path of drawerSurfaces) {
const source = readFileSync(path, "utf8");
assert.match(source, /id:\s*"partitions"/, `${path} must add a partitions tab`);
assert.match(
source,
/tableMetadataCapabilities\.value\.partitions/,
`${path} must gate the partitions tab on the capability`,
);
// A plain table must not show the tab at all, so both drawers probe the
// cheap partition status and gate on the result.
assert.match(source, /api\.getTablePartitionStatus/, `${path} must probe the partition status`);
assert.match(source, /isPartitionedTable|tableIsPartitioned/, `${path} must gate on the probed status`);
assert.match(source, /fetchPartitions|fetchTablePartitions/, `${path} must load partition metadata`);
}
});
test("both drawers probe the partition status on demand, not only on table change", () => {
// Regression: probing only in the table-change watcher meant opening the
// drawer for an already-selected table never resolved the status, so the
// Partitions tab was missing on every partitioned table.
const expectations: Array<[string, RegExp]> = [
["apps/desktop/src/components/grid/DataGrid.vue", /if \(!partitionStatusResolved\.value\) await probeTablePartitionStatus\(\)/],
[
"apps/desktop/src/components/objects/ObjectBrowser.vue",
/if \(!tablePartitionStatusResolved\.value\) await probeTablePartitionStatus\(\)/,
],
];
for (const [path, pattern] of expectations) {
assert.match(readFileSync(path, "utf8"), pattern, `${path} must probe on demand in selectTableInfoTab`);
}
});
test("both drawers render the shared partitions panel", () => {
const panels = readFileSync("apps/desktop/src/components/grid/DataGridTableInfoPanels.vue", "utf8");
assert.match(panels, /TablePartitionsPanel/);
const objectBrowser = readFileSync("apps/desktop/src/components/objects/ObjectBrowser.vue", "utf8");
assert.match(objectBrowser, /TablePartitionsPanel/);
});
test("the partitions tab is a first-class persisted table-info tab", () => {
const settings = readFileSync("apps/desktop/src/stores/settingsStore.ts", "utf8");
assert.match(settings, /TABLE_INFO_TABS = new Set<TableInfoTab>\(\[[^\]]*"partitions"/);
});