feat(editor): configurable default sort when opening tables

This commit is contained in:
二丫讲梵
2026-09-18 13:37:33 +08:00
committed by GitHub
parent 5cdee2e7a3
commit ff874e8af9
25 changed files with 417 additions and 23 deletions
@@ -579,6 +579,9 @@ const editDataGridCellDetailButtonVisible = ref(settingsStore.editorSettings.dat
const editDataGridCrosshairHighlight = ref(settingsStore.editorSettings.dataGridCrosshairHighlight);
const editPageSize = ref(settingsStore.editorSettings.pageSize);
const editTableOpenPageSize = ref(settingsStore.editorSettings.tableOpenPageSize);
const editTableOpenSortMode = ref(settingsStore.editorSettings.tableOpenSortMode);
const editTableDatabaseSortDirection = ref(settingsStore.editorSettings.tableDatabaseSortDirection);
const editTableLocalSortDirection = ref(settingsStore.editorSettings.tableLocalSortDirection);
const editQueryResultMaxRowsEnabled = ref(settingsStore.editorSettings.queryResultMaxRowsEnabled);
const editQueryResultMaxRows = ref(settingsStore.editorSettings.queryResultMaxRows);
const editExternalSqlEditorMaxMb = ref(settingsStore.editorSettings.externalSqlEditorMaxMb);
@@ -887,6 +890,9 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
dataGridShowWhitespace: editDataGridShowWhitespace.value,
pageSize: editPageSize.value,
tableOpenPageSize: editTableOpenPageSize.value,
tableOpenSortMode: editTableOpenSortMode.value,
tableDatabaseSortDirection: editTableDatabaseSortDirection.value,
tableLocalSortDirection: editTableLocalSortDirection.value,
queryResultMaxRowsEnabled: editQueryResultMaxRowsEnabled.value,
queryResultMaxRows: editQueryResultMaxRows.value,
externalSqlEditorMaxMb: editExternalSqlEditorMaxMb.value,
@@ -1516,6 +1522,9 @@ function syncEditorSettingsDraftFromStore() {
editDataGridShowWhitespace.value = settingsStore.editorSettings.dataGridShowWhitespace;
editPageSize.value = settingsStore.editorSettings.pageSize;
editTableOpenPageSize.value = settingsStore.editorSettings.tableOpenPageSize;
editTableOpenSortMode.value = settingsStore.editorSettings.tableOpenSortMode;
editTableDatabaseSortDirection.value = settingsStore.editorSettings.tableDatabaseSortDirection;
editTableLocalSortDirection.value = settingsStore.editorSettings.tableLocalSortDirection;
editQueryResultMaxRowsEnabled.value = settingsStore.editorSettings.queryResultMaxRowsEnabled;
editQueryResultMaxRows.value = settingsStore.editorSettings.queryResultMaxRows;
editExternalSqlEditorMaxMb.value = settingsStore.editorSettings.externalSqlEditorMaxMb;
@@ -1634,6 +1643,9 @@ const editorSettingsDraftRefs: EditorSettingsDraftRefMap = {
dataGridCrosshairHighlight: editDataGridCrosshairHighlight,
pageSize: editPageSize,
tableOpenPageSize: editTableOpenPageSize,
tableOpenSortMode: editTableOpenSortMode,
tableDatabaseSortDirection: editTableDatabaseSortDirection,
tableLocalSortDirection: editTableLocalSortDirection,
queryResultMaxRowsEnabled: editQueryResultMaxRowsEnabled,
queryResultMaxRows: editQueryResultMaxRows,
externalSqlEditorMaxMb: editExternalSqlEditorMaxMb,
@@ -2118,6 +2130,9 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editDataGridShowWhitespace.value = DEFAULT_EDITOR_SETTINGS.dataGridShowWhitespace;
editPageSize.value = DEFAULT_EDITOR_SETTINGS.pageSize;
editTableOpenPageSize.value = DEFAULT_EDITOR_SETTINGS.tableOpenPageSize;
editTableOpenSortMode.value = DEFAULT_EDITOR_SETTINGS.tableOpenSortMode;
editTableDatabaseSortDirection.value = DEFAULT_EDITOR_SETTINGS.tableDatabaseSortDirection;
editTableLocalSortDirection.value = DEFAULT_EDITOR_SETTINGS.tableLocalSortDirection;
editQueryResultMaxRowsEnabled.value = DEFAULT_EDITOR_SETTINGS.queryResultMaxRowsEnabled;
editQueryResultMaxRows.value = DEFAULT_EDITOR_SETTINGS.queryResultMaxRows;
editInfiniteScroll.value = DEFAULT_EDITOR_SETTINGS.infiniteScroll;
@@ -2212,6 +2227,9 @@ function resetAllDefaults() {
editDataGridShowWhitespace.value = DEFAULT_EDITOR_SETTINGS.dataGridShowWhitespace;
editPageSize.value = DEFAULT_EDITOR_SETTINGS.pageSize;
editTableOpenPageSize.value = DEFAULT_EDITOR_SETTINGS.tableOpenPageSize;
editTableOpenSortMode.value = DEFAULT_EDITOR_SETTINGS.tableOpenSortMode;
editTableDatabaseSortDirection.value = DEFAULT_EDITOR_SETTINGS.tableDatabaseSortDirection;
editTableLocalSortDirection.value = DEFAULT_EDITOR_SETTINGS.tableLocalSortDirection;
editQueryResultMaxRowsEnabled.value = DEFAULT_EDITOR_SETTINGS.queryResultMaxRowsEnabled;
editQueryResultMaxRows.value = DEFAULT_EDITOR_SETTINGS.queryResultMaxRows;
editExternalSqlEditorMaxMb.value = DEFAULT_EDITOR_SETTINGS.externalSqlEditorMaxMb;
@@ -7335,6 +7353,35 @@ onUnmounted(() => {
@update:model-value="updatePageSizeDraft"
/>
</div>
<div class="settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="tableOpenSortMode">{{ t("settings.tableOpenSortMode") }}</Label>
<p class="text-xs text-muted-foreground">{{ t("settings.tableOpenSortDescription") }}</p>
</div>
<select id="tableOpenSortMode" v-model="editTableOpenSortMode" class="h-8 rounded-md border bg-background px-2 text-xs">
<option value="none">{{ t("settings.tableSortUnchanged") }}</option>
<option value="database">{{ t("settings.tableSortDatabase") }}</option>
<option value="local">{{ t("settings.tableSortLocal") }}</option>
</select>
</div>
<div class="settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="tableDatabaseSortDirection">{{ t("settings.tableDatabaseSortDirection") }}</Label>
</div>
<select id="tableDatabaseSortDirection" v-model="editTableDatabaseSortDirection" class="h-8 rounded-md border bg-background px-2 text-xs">
<option value="asc">{{ t("settings.tableSortAscending") }}</option>
<option value="desc">{{ t("settings.tableSortDescending") }}</option>
</select>
</div>
<div class="settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="tableLocalSortDirection">{{ t("settings.tableLocalSortDirection") }}</Label>
</div>
<select id="tableLocalSortDirection" v-model="editTableLocalSortDirection" class="h-8 rounded-md border bg-background px-2 text-xs">
<option value="asc">{{ t("settings.tableSortAscending") }}</option>
<option value="desc">{{ t("settings.tableSortDescending") }}</option>
</select>
</div>
<div data-settings-search-id="default-auto-keep-results" :class="['settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2', settingsSearchTargetClass('default-auto-keep-results')]">
<div class="min-w-0 space-y-1">
<Label for="default-auto-keep-results">{{ t("settings.defaultAutoKeepResults") }}</Label>
@@ -8,11 +8,13 @@ const mocks = vi.hoisted(() => ({
activeTabId: "",
buildTableSelectSql: vi.fn(),
databaseType: "postgres" as string,
tableOpenSortMode: "none" as "none" | "database" | "local",
ensureConnected: vi.fn(),
executeTabSql: vi.fn(),
getColumns: vi.fn(),
invalidateCompletionTableCache: vi.fn(),
listIndexes: vi.fn(),
setErrorResult: vi.fn(),
setTableMeta: vi.fn(),
updateSql: vi.fn(),
}));
@@ -75,16 +77,17 @@ vi.mock("@/stores/queryStore", () => ({
}),
updateSql: mocks.updateSql,
executeTabSql: mocks.executeTabSql,
setErrorResult: vi.fn(),
setErrorResult: mocks.setErrorResult,
invalidateTableStructure: vi.fn(),
}),
}));
vi.mock("@/stores/settingsStore", () => ({
useSettingsStore: () => ({ editorSettings: { dataTabReuseMode: "same-table" } }),
useSettingsStore: () => ({ editorSettings: { dataTabReuseMode: "same-table", tableOpenSortMode: mocks.tableOpenSortMode, tableDatabaseSortDirection: "desc", tableLocalSortDirection: "asc" } }),
}));
vi.mock("@/lib/table/tableSelectSql", () => ({
vi.mock("@/lib/table/tableSelectSql", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/table/tableSelectSql")>()),
buildTableSelectSql: mocks.buildTableSelectSql,
}));
@@ -104,6 +107,7 @@ describe("useNavigationTargets openTableTarget", () => {
vi.clearAllMocks();
mocks.tabs.length = 0;
mocks.databaseType = "postgres";
mocks.tableOpenSortMode = "none";
mocks.ensureConnected.mockResolvedValue(undefined);
mocks.buildTableSelectSql.mockImplementation(async ({ tableName }: { tableName: string }) => `SELECT * FROM ${tableName}`);
mocks.executeTabSql.mockResolvedValue(undefined);
@@ -111,6 +115,33 @@ describe("useNavigationTargets openTableTarget", () => {
mocks.listIndexes.mockResolvedValue([]);
});
it.each(["oracle", "oceanbase-oracle", "xugu", "neo4j"])("keeps %s synthetic row identifiers out of default database ordering", async (databaseType) => {
mocks.databaseType = databaseType;
mocks.tableOpenSortMode = "database";
mocks.getColumns.mockResolvedValue([{ ...column("name"), is_primary_key: false }]);
await useNavigationTargets(dialogs).openTableTarget({ connectionId: "connection-1", database: "app", tableName: "users", tableType: "TABLE" });
expect(mocks.executeTabSql).toHaveBeenCalled();
expect(mocks.buildTableSelectSql).toHaveBeenCalled();
for (const [options] of mocks.buildTableSelectSql.mock.calls) {
expect(options.orderBy).toBeUndefined();
}
expect(mocks.tabs[0]?.orderByInput).toBeUndefined();
});
it.each(["oracle", "oceanbase-oracle"])("uses the %s physical primary index for default ordering", async (databaseType) => {
mocks.databaseType = databaseType;
mocks.tableOpenSortMode = "database";
mocks.getColumns.mockResolvedValue([{ ...column("id"), is_primary_key: false }]);
mocks.listIndexes.mockResolvedValue([{ name: "users_pk", columns: ["id"], is_unique: true, is_primary: true }]);
await useNavigationTargets(dialogs).openTableTarget({ connectionId: "connection-1", database: "app", tableName: "users", tableType: "TABLE" });
expect(mocks.setErrorResult).not.toHaveBeenCalled();
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(expect.objectContaining({ orderBy: '"id" DESC' }));
});
it("loads stable row identity before the first PostgreSQL table query", async () => {
let releaseColumns: (columns: unknown[]) => void = () => {};
mocks.getColumns.mockReturnValueOnce(
@@ -162,7 +193,7 @@ describe("useNavigationTargets openTableTarget", () => {
});
it("keeps row identity pending when shared metadata loading fails", async () => {
mocks.getColumns.mockRejectedValueOnce(new Error("metadata unavailable"));
mocks.getColumns.mockRejectedValue(new Error("metadata unavailable"));
await useNavigationTargets(dialogs).openLineageTarget({ connectionId: "connection-1", database: "app", schema: "public", tableName: "users" });
@@ -170,6 +201,17 @@ describe("useNavigationTargets openTableTarget", () => {
expect(mocks.tabs[0]?.tableMetaPending).toBe(true);
});
it.each(["none", "database", "local"] as const)("opens the table when metadata preload fails in %s sort mode", async (mode) => {
mocks.databaseType = "sqlite";
mocks.tableOpenSortMode = mode;
mocks.getColumns.mockRejectedValue(new Error("metadata unavailable"));
await useNavigationTargets(dialogs).openLineageTarget({ connectionId: "connection-1", database: "app", tableName: "users" });
expect(mocks.executeTabSql).toHaveBeenCalledTimes(1);
expect(mocks.tabs[0]?.tableMetaPending).toBe(true);
});
it("uses the SQLite attached database as the table namespace when a target omits schema", async () => {
mocks.databaseType = "sqlite";
@@ -5,6 +5,8 @@ import type { QueryTab, TreeNode } from "@/types/database";
const mocks = vi.hoisted(() => ({
databaseType: "oceanbase" as string,
tableOpenSortMode: "none" as "none" | "database" | "local",
sortTabResultLocally: vi.fn(),
callOrder: [] as string[],
tabs: [] as QueryTab[],
activeTabId: null as string | null,
@@ -90,12 +92,13 @@ vi.mock("@/stores/queryStore", () => ({
if (tab) tab.sql = sql;
},
executeTabSql: mocks.executeTabSql,
sortTabResultLocally: mocks.sortTabResultLocally,
setErrorResult: mocks.setErrorResult,
}),
}));
vi.mock("@/stores/settingsStore", () => ({
useSettingsStore: () => ({ editorSettings: { dataTabReuseMode: mocks.dataTabReuseMode, openDataTabsNextToActive: mocks.openDataTabsNextToActive, pageSize: 100 } }),
useSettingsStore: () => ({ editorSettings: { tableOpenSortMode: mocks.tableOpenSortMode, tableDatabaseSortDirection: "desc", tableLocalSortDirection: "asc", dataTabReuseMode: mocks.dataTabReuseMode, openDataTabsNextToActive: mocks.openDataTabsNextToActive, pageSize: 100 } }),
}));
vi.mock("@/lib/database/jdbcDialect", () => ({
@@ -117,8 +120,12 @@ vi.mock("@/lib/common/utils", () => ({ uuid: () => "open-data-id" }));
vi.mock("@/lib/backend/debugLog", () => ({ appendDebugLog: vi.fn(), isDebugLoggingEnabled: () => false }));
// dataTabOpenPolicy 使用真实实现,覆盖设置开关对应的复用范围
vi.mock("@/lib/sidebar/treeNodeContext", () => ({ hasTreeNodeDatabaseContext: () => true }));
vi.mock("@/lib/table/tableSelectSql", () => ({ buildTableSelectSql: mocks.buildTableSelectSql }));
vi.mock("@/lib/table/tableSelectSql", async (importOriginal) => ({ ...(await importOriginal<typeof import("@/lib/table/tableSelectSql")>()), buildTableSelectSql: mocks.buildTableSelectSql }));
vi.mock("@/lib/table/tableEditing", () => ({
physicalTablePrimaryKeys: (columns: Array<{ name: string; is_primary_key: boolean }>, indexes: Array<{ columns: string[]; is_primary: boolean }> = []) => {
const columnPrimaryKeys = columns.filter((column) => column.is_primary_key).map((column) => column.name);
return columnPrimaryKeys.length > 0 ? columnPrimaryKeys : (indexes.find((index) => index.is_primary && index.columns.length > 0)?.columns ?? []);
},
usesSyntheticRowIdKey: () => false,
shouldIncludeSyntheticRowId: () => false,
}));
@@ -147,6 +154,7 @@ describe("useSidebarDataOpenRuntime", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.databaseType = "oceanbase";
mocks.tableOpenSortMode = "none";
mocks.callOrder.length = 0;
mocks.tabs.length = 0;
mocks.activeTabId = null;
@@ -178,6 +186,48 @@ describe("useSidebarDataOpenRuntime", () => {
});
});
it.each(["database", "local"] as const)("applies the %s default using its independent direction", async (mode) => {
mocks.tableOpenSortMode = mode;
mocks.executeTabSql.mockImplementation(async () => {
const tab = mocks.tabs[0]!;
tab.isExecuting = false;
tab.executionId = undefined;
tab.result = { columns: ["id"], rows: [[2], [1]], affected_rows: 0, execution_time_ms: 0 } as any;
});
await useSidebarDataOpenRuntime().openData(tableNode);
if (mode === "database") {
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(expect.objectContaining({ orderBy: '"id" DESC' }));
expect(mocks.tabs[0]).toMatchObject({ resultSortColumn: "id", resultSortDirection: "desc", resultSortMode: "database" });
} else {
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(expect.objectContaining({ orderBy: undefined }));
expect(mocks.sortTabResultLocally).toHaveBeenCalledWith("tab-1", "id", 0, "asc");
}
});
it("applies database sorting from a physical primary index when columns omit the key flag", async () => {
mocks.databaseType = "oracle";
mocks.tableOpenSortMode = "database";
mocks.loadTableMetadata.mockResolvedValue({
metadata: {
schema: "public",
tableName: "users",
tableType: "TABLE",
database: "app",
columns: [{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: false, extra: null }],
indexes: [{ name: "users_pk", columns: ["id"], is_unique: true, is_primary: true }],
primaryKeys: ["id"],
cachedAt: Date.now(),
},
cacheStatus: "miss",
ageMs: 0,
});
await useSidebarDataOpenRuntime().openData(tableNode);
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(expect.objectContaining({ orderBy: '"id" DESC' }));
expect(mocks.tabs[0]?.tableMeta?.physicalPrimaryKeys).toEqual(["id"]);
});
it("creates a new sidebar tab for the same table in always-new mode", async () => {
mocks.dataTabReuseMode = "always-new";
@@ -4,8 +4,9 @@ import { invalidateTableMetadataCache, loadTableMetadata } from "@/lib/metadata/
import { canApplyDataTabMetadata, canReuseActiveMongoTab, type DataTabReuseMode } from "@/lib/sidebar/dataTabOpenPolicy";
import { isNoSnapshotErrorResult, isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
import { buildTableSelectSql } from "@/lib/table/tableSelectSql";
import { resolveTableDefaultSort, applyTableDefaultSortResult } from "@/lib/table/tableDefaultSort";
import { tableDataLargeValuePreviewOptions } from "@/lib/dataGrid/dataGridLargeValues";
import { editableRowIdentifierColumns, shouldIncludeSyntheticRowId } from "@/lib/table/tableEditing";
import { editableRowIdentifierColumns, physicalTablePrimaryKeys, shouldIncludeSyntheticRowId } from "@/lib/table/tableEditing";
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
import { uuid } from "@/lib/common/utils";
import { beginDataTabNavigation, endDataTabNavigation, isCurrentDataTabNavigation } from "@/lib/tabs/dataTabNavigationGeneration";
@@ -142,6 +143,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
if (config.db_type === "neo4j") {
const columns = await api.getColumns(target.connectionId, target.database, querySchema, target.tableName);
const primaryKeys = editableRowIdentifierColumns(effectiveDbType, columns, undefined, targetTableType);
const defaultSort = resolveTableDefaultSort(settingsStore.editorSettings, effectiveDbType, physicalTablePrimaryKeys(columns), identifierQuote);
const sql = await buildTableSelectSql({
databaseType: effectiveDbType,
driverProfile: config.driver_profile,
@@ -155,6 +157,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
tableType: targetTableType,
columns: columns.map((column) => column.name),
primaryKeys,
orderBy: defaultSort.orderBy,
whereInput: target.whereInput,
limit: pageLimit,
});
@@ -170,24 +173,34 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
primaryKeys,
});
firstExecuteStarted = true;
if (targetTab) targetTab.orderByInput = defaultSort.orderBy;
await queryStore.executeTabSql(tabId, sql, { pagination: { limit: pageLimit, offset: 0 } });
if (isCurrentTarget() && targetTab) applyTableDefaultSortResult(targetTab, defaultSort, queryStore.sortTabResultLocally);
return;
}
const eagerMetadata =
effectiveDbType === "mysql" || effectiveDbType === "postgres"
? await loadTableMetadata({
connectionId: target.connectionId,
database: target.database,
schema: querySchema,
tableName: target.tableName,
tableType: targetTableType,
databaseType: effectiveDbType,
driverProfile: config.driver_profile || config.db_type,
catalog: target.catalog,
})
: undefined;
let eagerMetadata: Awaited<ReturnType<typeof loadTableMetadata>> | undefined;
if (effectiveDbType === "mysql" || effectiveDbType === "postgres" || (settingsStore.editorSettings.tableOpenSortMode ?? "none") !== "none") {
try {
eagerMetadata = await loadTableMetadata({
connectionId: target.connectionId,
database: target.database,
schema: querySchema,
tableName: target.tableName,
tableType: targetTableType,
databaseType: effectiveDbType ?? config.db_type,
driverProfile: config.driver_profile || config.db_type,
catalog: target.catalog,
});
} catch (error) {
// Metadata is needed for optional default sorting, but it must not
// prevent the data preview from opening. The later metadata refresh
// keeps the tab pending for edits and can retry independently.
console.warn("[DBX] unable to preload table metadata for default sort", error);
}
}
const eagerColumns = eagerMetadata?.metadata.columns ?? [];
const eagerPrimaryKeys = eagerMetadata?.metadata.primaryKeys ?? [];
const defaultSort = resolveTableDefaultSort(settingsStore.editorSettings, effectiveDbType, physicalTablePrimaryKeys(eagerColumns, eagerMetadata?.metadata.indexes), identifierQuote);
const sql = await buildTableSelectSql({
databaseType: effectiveDbType,
driverProfile: config.driver_profile,
@@ -200,6 +213,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
tableType: targetTableType,
columns: eagerColumns.map((column) => column.name),
primaryKeys: eagerPrimaryKeys,
orderBy: defaultSort.orderBy,
...tableDataLargeValuePreviewOptions(effectiveDbType, eagerColumns, eagerPrimaryKeys, pageLimit),
whereInput: target.whereInput,
limit: pageLimit,
@@ -219,6 +233,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
// 取消计数快照:isCancelling 是瞬态的(取消失败/查询先完成会被清掉),
// 比对计数才能跨越 executeTabSql 生命周期识别"执行期间用户请求过停止"
const cancelCountBeforeExecute = queryStore.tabs.find((tab) => tab.id === tabId)?.cancelRequestCount ?? 0;
if (targetTab) targetTab.orderByInput = defaultSort.orderBy;
await queryStore.executeTabSql(tabId, sql, { pagination: { limit: pageLimit, offset: 0 } });
if (!isCurrentTarget()) return;
// 首次查询被停止/失败(executeTabSql 以 Error 结果表达,不抛出)时,
@@ -228,6 +243,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
const firstResult = tabAfterFirstExecute?.result;
const cancelRequestedDuringExecute = (tabAfterFirstExecute?.cancelRequestCount ?? 0) > cancelCountBeforeExecute;
const firstQueryFailed = cancelRequestedDuringExecute || tabAfterFirstExecute?.isCancelling === true || (firstResult !== undefined && isQueryExecutionErrorResult(firstResult));
if (!firstQueryFailed && targetTab) applyTableDefaultSortResult(targetTab, defaultSort, queryStore.sortTabResultLocally);
// executeTabSql surfaces query failures as an "Error" result instead of throwing.
// A snapshot-less lake table fails the data preview above but its metadata still
// reads fine — retry with LIMIT 0 so the user sees the table structure (columns +
@@ -298,11 +314,13 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
primaryKeys,
columns: columns.map((column) => column.name),
includeRowId: true,
orderBy: defaultSort.orderBy,
limit: pageLimit,
});
if (!isCurrentTarget()) return;
queryStore.updateSql(tabId, newSql);
await queryStore.executeTabSql(tabId, newSql, { pagination: { limit: pageLimit, offset: 0 } });
if (isCurrentTarget() && targetTab) applyTableDefaultSortResult(targetTab, defaultSort, queryStore.sortTabResultLocally);
}
} catch (reason) {
console.error("[DBX] ERROR fetching table metadata:", reason);
@@ -11,7 +11,8 @@ import { canApplyDataTabMetadata, dataTabMetadataNeedsRefresh, findExistingDataT
import type { SidebarDataOpenRequest } from "@/lib/sidebar/sidebarDataOpenCoordinator";
import { hasTreeNodeDatabaseContext } from "@/lib/sidebar/treeNodeContext";
import { buildTableSelectSql } from "@/lib/table/tableSelectSql";
import { shouldIncludeSyntheticRowId } from "@/lib/table/tableEditing";
import { resolveTableDefaultSort, applyTableDefaultSortResult } from "@/lib/table/tableDefaultSort";
import { physicalTablePrimaryKeys, shouldIncludeSyntheticRowId } from "@/lib/table/tableEditing";
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
import { tableDataLargeValuePreviewOptions } from "@/lib/dataGrid/dataGridLargeValues";
import { canActivateExistingDataTableTab } from "@/lib/tabs/dataTabActivation";
@@ -314,6 +315,7 @@ export function useSidebarDataOpenRuntime() {
if (!config) throw new Error("Connection config not found");
const limit = tableOpenPageLimit(settingsStore.editorSettings.tableOpenPageSize);
const defaultSortMode = settingsStore.editorSettings.tableOpenSortMode ?? "none";
const shouldRefreshTableMeta = !cachedTableMeta;
// Dameng metadata calls must remain serialized behind the table query.
const deferTableMetaRefresh = effectiveDbType === "dameng";
@@ -334,7 +336,9 @@ export function useSidebarDataOpenRuntime() {
}
const metadataRefresh = shouldRefreshTableMeta && !deferTableMetaRefresh ? refreshTableMetaInBackground(tabId) : undefined;
if (!cachedTableMeta && (effectiveDbType === "mysql" || effectiveDbType === "postgres")) {
if (!cachedTableMeta && (effectiveDbType === "mysql" || effectiveDbType === "postgres" || defaultSortMode !== "none")) {
// No query is in flight yet, so deferred drivers can safely load metadata serially.
if (deferTableMetaRefresh) await refreshTableMetaInBackground(tabId);
await metadataRefresh;
}
@@ -347,6 +351,8 @@ export function useSidebarDataOpenRuntime() {
const loadedTableMeta = cachedTableMeta ?? queryStore.tabs.find((item) => item.id === tabId)?.tableMeta;
const columns = loadedTableMeta?.columns ?? [];
const primaryKeys = loadedTableMeta?.primaryKeys ?? [];
const defaultSort = resolveTableDefaultSort(settingsStore.editorSettings, effectiveDbType, loadedTableMeta?.physicalPrimaryKeys ?? physicalTablePrimaryKeys(columns), connectionStore.connectionIdentifierQuote?.(node.connectionId));
const defaultOrderBy = defaultSort.orderBy;
const includeRowId = shouldIncludeSyntheticRowId(effectiveDbType, primaryKeys, tableType);
const sql = await buildTableSelectSql({
databaseType: effectiveDbType,
@@ -364,6 +370,7 @@ export function useSidebarDataOpenRuntime() {
limit,
includeRowId,
injectDefaultTimeSeriesWhere: true,
orderBy: defaultOrderBy,
});
// SQL 构建是异步后端调用:期间更晚的导航(openData/openTableTarget)
// 接管会替换 executionId,旧流程不得再覆盖 SQL/启动查询
@@ -380,6 +387,8 @@ export function useSidebarDataOpenRuntime() {
});
logPhase("sql-built", { tabId, columnCount: columns.length, primaryKeyCount: primaryKeys.length });
queryStore.updateSql(tabId, sql);
const openedTab = queryStore.tabs.find((item) => item.id === tabId);
if (openedTab && defaultOrderBy) openedTab.orderByInput = defaultOrderBy;
logPhase("sql-updated", { tabId });
openDataLog("info", "execute:start", { traceId, tabId, elapsed: elapsed() });
@@ -389,6 +398,10 @@ export function useSidebarDataOpenRuntime() {
pagination: { limit, offset: 0 },
});
openDataLog("info", "execute:done", { traceId, tabId, elapsed: elapsed() });
const sortedTab = queryStore.tabs.find((item) => item.id === tabId);
if ((request?.isCurrent() ?? true) && sortedTab && sortedTab === openedTab && sortedTab.sql === sql) {
applyTableDefaultSortResult(sortedTab, defaultSort, queryStore.sortTabResultLocally);
}
logPhase("execute-tab-sql", { tabId });
if (shouldRefreshTableMeta && deferTableMetaRefresh && canApplyTableMetadata(tabId)) {
void refreshTableMetaInBackground(tabId);
+9
View File
@@ -6704,6 +6704,15 @@ export default withEnglishFallback({
syncSuccess: "Verilənlərin sinxronlaşdırılması uğurla icra edildi",
},
settings: {
tableOpenSortMode: "Cədvəli açarkən sıralama",
tableDatabaseSortDirection: "Verilənlər bazası sıralama istiqaməti",
tableLocalSortDirection: "Cari səhifənin sıralama istiqaməti",
tableOpenSortDescription: "Yeni cədvəllər: bazada bütün əsas açarlar, yerli sıralamada ilk əsas açar istifadə olunur. Dəstəklənmədikdə mövcud sıra saxlanılır.",
tableSortUnchanged: "Mövcud davranışı saxla",
tableSortDatabase: "Verilənlər bazasında sırala",
tableSortLocal: "Cari səhifəni sırala",
tableSortAscending: "Artan ASC",
tableSortDescending: "Azalan DESC",
title: "Parametrlər",
searchSettings: "Parametrləri axtar",
searchSettingsNoResults: "Uyğun parametr yoxdur",
+9
View File
@@ -6930,6 +6930,15 @@ export default {
syncSuccess: "Data sync executed successfully",
},
settings: {
tableOpenSortMode: "Default table sort",
tableDatabaseSortDirection: "Default database sort direction",
tableLocalSortDirection: "Default current page sort direction",
tableOpenSortDescription: "For newly opened tables, database sorting uses the complete primary key; current page sorting uses its first column and only loaded rows. Keep existing behavior when unavailable or unsupported.",
tableSortUnchanged: "Keep existing behavior",
tableSortDatabase: "Database sorting",
tableSortLocal: "Current page sorting",
tableSortAscending: "Ascending ASC",
tableSortDescending: "Descending DESC",
title: "Settings",
searchSettings: "Search settings",
searchSettingsNoResults: "No matching settings",
+9
View File
@@ -6491,6 +6491,15 @@ export default withEnglishFallback({
syncSuccess: "Sincronización de datos ejecutada exitosamente",
},
settings: {
tableOpenSortMode: "Orden predeterminado de tablas",
tableDatabaseSortDirection: "Dirección del orden en la base de datos",
tableLocalSortDirection: "Dirección del orden de la página actual",
tableOpenSortDescription: "En tablas nuevas, el orden en la base de datos usa toda la clave primaria; el local usa su primera columna y solo filas cargadas. Si no está disponible, se conserva el orden existente.",
tableSortUnchanged: "Mantener comportamiento actual",
tableSortDatabase: "Ordenar en la base de datos",
tableSortLocal: "Ordenar página actual",
tableSortAscending: "Ascendente ASC",
tableSortDescending: "Descendente DESC",
title: "Configuración",
searchSettings: "Buscar configuración",
searchSettingsNoResults: "No hay ajustes coincidentes",
+9
View File
@@ -6491,6 +6491,15 @@ export default withEnglishFallback({
syncSuccess: "Sincronizzazione dei dati eseguita con successo",
},
settings: {
tableOpenSortMode: "Ordinamento predefinito delle tabelle",
tableDatabaseSortDirection: "Direzione nel database",
tableLocalSortDirection: "Direzione nella pagina corrente",
tableOpenSortDescription: "Per le nuove tabelle, il database usa tutta la chiave primaria; l’ordinamento locale usa la prima colonna e solo le righe caricate. Se non disponibile, mantiene l’ordine esistente.",
tableSortUnchanged: "Mantieni comportamento attuale",
tableSortDatabase: "Ordina nel database",
tableSortLocal: "Ordina pagina corrente",
tableSortAscending: "Crescente ASC",
tableSortDescending: "Decrescente DESC",
title: "Impostazioni",
searchSettings: "Cerca impostazioni",
searchSettingsNoResults: "Nessuna impostazione corrispondente",
+9
View File
@@ -6529,6 +6529,15 @@ export default withEnglishFallback({
syncSuccess: "データ同期が正常に実行されました",
},
settings: {
tableOpenSortMode: "テーブルの既定の並び順",
tableDatabaseSortDirection: "データベースの既定のソート方向",
tableLocalSortDirection: "現在のページの既定のソート方向",
tableOpenSortDescription: "新しく開くテーブルに適用します。データベースでは主キー全体、ページ内では最初の主キー列で読み込み済みの行を並べ替えます。利用できない場合は従来の順序を維持します。",
tableSortUnchanged: "現在の動作を維持",
tableSortDatabase: "データベース全体でソート",
tableSortLocal: "現在のページでソート",
tableSortAscending: "昇順 ASC",
tableSortDescending: "降順 DESC",
title: "設定",
searchSettings: "設定を検索",
searchSettingsNoResults: "一致する設定がありません",
+9
View File
@@ -6241,6 +6241,15 @@ export default withEnglishFallback({
syncSuccess: "데이터 동기화를 성공적으로 실행했습니다",
},
settings: {
tableOpenSortMode: "테이블 기본 정렬",
tableDatabaseSortDirection: "데이터베이스 기본 정렬 방향",
tableLocalSortDirection: "현재 페이지 기본 정렬 방향",
tableOpenSortDescription: "새 테이블에 적용합니다. 데이터베이스 정렬은 전체 기본 키를, 로컬 정렬은 첫 번째 기본 키 열과 로드된 행만 사용합니다. 지원하지 않으면 기존 순서를 유지합니다.",
tableSortUnchanged: "기존 동작 유지",
tableSortDatabase: "데이터베이스 정렬",
tableSortLocal: "현재 페이지 정렬",
tableSortAscending: "오름차순 ASC",
tableSortDescending: "내림차순 DESC",
title: "설정",
searchSettings: "설정 검색",
searchSettingsNoResults: "일치하는 설정이 없습니다",
+9
View File
@@ -6493,6 +6493,15 @@ export default withEnglishFallback({
syncSuccess: "Sincronização de dados executada com sucesso",
},
settings: {
tableOpenSortMode: "Ordenação padrão das tabelas",
tableDatabaseSortDirection: "Direção padrão no banco de dados",
tableLocalSortDirection: "Direção padrão na página atual",
tableOpenSortDescription: "Em novas tabelas, o banco usa toda a chave primária; a ordenação local usa a primeira coluna e apenas linhas carregadas. Quando indisponível, mantém a ordem existente.",
tableSortUnchanged: "Manter comportamento atual",
tableSortDatabase: "Ordenar no banco de dados",
tableSortLocal: "Ordenar página atual",
tableSortAscending: "Crescente ASC",
tableSortDescending: "Decrescente DESC",
title: "Configurações",
searchSettings: "Pesquisar configurações",
searchSettingsNoResults: "Nenhuma configuração encontrada",
+9
View File
@@ -6586,6 +6586,15 @@ export default withEnglishFallback({
syncSuccess: "Veri eşitlemesi başarıyla çalıştırıldı",
},
settings: {
tableOpenSortMode: "Varsayılan tablo sıralaması",
tableDatabaseSortDirection: "Varsayılan veritabanı sıralama yönü",
tableLocalSortDirection: "Geçerli sayfanın varsayılan sıralama yönü",
tableOpenSortDescription: "Yeni tablolarda veritabanı sıralaması tüm birincil anahtarı, yerel sıralama ilk anahtar sütununu ve yüklenen satırları kullanır. Desteklenmiyorsa mevcut sıra korunur.",
tableSortUnchanged: "Mevcut davranışı koru",
tableSortDatabase: "Veritabanında sırala",
tableSortLocal: "Geçerli sayfayı sırala",
tableSortAscending: "Artan ASC",
tableSortDescending: "Azalan DESC",
title: "Ayarlar",
searchSettings: "Ayarlarda ara",
searchSettingsNoResults: "Eşleşen ayar yok",
+9
View File
@@ -6904,6 +6904,15 @@ export default withEnglishFallback({
syncSuccess: "数据同步执行成功",
},
settings: {
tableOpenSortMode: "打开表默认排序",
tableDatabaseSortDirection: "全表默认排序方向",
tableLocalSortDirection: "当前页默认排序方向",
tableOpenSortDescription: "仅影响新打开的表。全表按完整主键排序,当前页按第一个主键字段排序且仅处理已加载数据;无主键或不支持时保持原行为。",
tableSortUnchanged: "保持现有行为",
tableSortDatabase: "全表排序",
tableSortLocal: "当前页排序",
tableSortAscending: "升序 ASC",
tableSortDescending: "降序 DESC",
title: "设置",
searchSettings: "搜索设置",
searchSettingsNoResults: "未找到匹配的设置项",
+9
View File
@@ -5823,6 +5823,15 @@ export default withEnglishFallback({
syncSuccess: "資料同步執行成功",
},
settings: {
tableOpenSortMode: "開啟表格預設排序",
tableDatabaseSortDirection: "全表預設排序方向",
tableLocalSortDirection: "目前頁面預設排序方向",
tableOpenSortDescription: "僅影響新開啟的表格。全表按完整主鍵排序,目前頁面按第一個主鍵欄位排序且僅處理已載入資料;無主鍵或不支援時維持原行為。",
tableSortUnchanged: "維持現有行為",
tableSortDatabase: "全表排序",
tableSortLocal: "目前頁面排序",
tableSortAscending: "升冪 ASC",
tableSortDescending: "降冪 DESC",
title: "設定",
searchSettings: "搜尋設定",
searchSettingsNoResults: "找不到相符的設定項目",
@@ -0,0 +1,56 @@
import { describe, expect, it, vi } from "vitest";
import { applyTableDefaultSortResult, resolveTableDefaultSort } from "@/lib/table/tableDefaultSort";
import { editableRowIdentifierColumns, physicalTablePrimaryKeys } from "@/lib/table/tableEditing";
import type { ColumnInfo, QueryTab } from "@/types/database";
const settings = { tableOpenSortMode: "database", tableDatabaseSortDirection: "desc", tableLocalSortDirection: "asc" } as const;
const column: ColumnInfo = { name: "id", data_type: "integer", is_nullable: false, column_default: null, is_primary_key: false, extra: null };
describe("table default sorting", () => {
it.each(["oracle", "oceanbase-oracle", "xugu", "neo4j"] as const)("preserves existing ordering for %s synthetic row identities in both modes", (databaseType) => {
const keys = editableRowIdentifierColumns(databaseType, [column], [], "TABLE");
expect(keys).toHaveLength(1);
for (const tableOpenSortMode of ["database", "local"] as const) {
const sort = resolveTableDefaultSort({ ...settings, tableOpenSortMode }, databaseType, physicalTablePrimaryKeys([column]));
const tab = { id: "tab-1", result: { columns: keys, rows: [["2"], ["1"]] } } as QueryTab;
const sortLocally = vi.fn();
applyTableDefaultSortResult(tab, sort, sortLocally);
expect(sort.orderBy).toBeUndefined();
expect(sortLocally).not.toHaveBeenCalled();
expect(tab.orderByInput).toBeUndefined();
expect(tab.resultSortMode).toBeUndefined();
expect(tab.result?.rows).toEqual([["2"], ["1"]]);
}
});
it.each(["oracle", "neo4j"] as const)("still sorts real %s primary keys", (databaseType) => {
const columns = [{ ...column, is_primary_key: true }];
const sort = resolveTableDefaultSort(settings, databaseType, physicalTablePrimaryKeys(columns));
expect(sort.orderBy).toBe(databaseType === "oracle" ? '"id" DESC' : "n.`id` DESC");
});
it.each(["oracle", "oceanbase-oracle"] as const)("sorts %s primary indexes when column metadata omits the key flag", (databaseType) => {
const columns = [{ ...column, name: "id" }];
const sort = resolveTableDefaultSort(settings, databaseType, physicalTablePrimaryKeys(columns, [{ columns: ["id"], is_primary: true }]));
expect(sort.orderBy).toBe('"id" DESC');
});
it("does not sort by a unique row identifier when the table lacks a physical primary key", () => {
const columns = [{ ...column, name: "email" }];
const rowIdentifiers = editableRowIdentifierColumns("postgres", columns, [{ name: "users_email_key", columns: ["email"], is_unique: true, is_primary: false, filter: null }]);
expect(rowIdentifiers).toEqual(["email"]);
const sort = resolveTableDefaultSort(settings, "postgres", physicalTablePrimaryKeys(columns));
expect(sort.orderBy).toBeUndefined();
expect(sort.columns).toEqual([]);
});
it("preserves composite ordering and escapes identifiers", () => {
const sort = resolveTableDefaultSort(settings, "postgres", ['tenant"id', "id"]);
expect(sort.orderBy).toBe('"tenant""id" DESC, "id" DESC');
const tab = { id: "tab-1", result: { columns: ['tenant"id', "id"], rows: [] } } as unknown as QueryTab;
applyTableDefaultSortResult(tab, sort, vi.fn());
expect(tab.orderByInput).toBe(sort.orderBy);
expect(tab.resultSortColumn).toBeUndefined();
});
});
@@ -1,6 +1,6 @@
import type { ColumnInfo, DatabaseType, IndexInfo, QueryTab } from "@/types/database";
import * as api from "@/lib/backend/api";
import { editableRowIdentifierColumns } from "@/lib/table/tableEditing";
import { editableRowIdentifierColumns, physicalTablePrimaryKeys } from "@/lib/table/tableEditing";
import { createMetadataLoadTrace, logMetadataLoadTrace, MetadataLoadCoordinator, type MetadataLoadCacheStatus, type MetadataLoadTraceLogger } from "./metadataLoadCoordinator";
import { metadataScopeKey, metadataScopeParts, type MetadataScopeInput } from "./metadataLoadScope";
import { metadataCacheInvalidationMatcher, MetadataResultCache, type MetadataCacheInvalidation } from "./metadataResultCache";
@@ -275,6 +275,7 @@ export function tableMetadataToDataTabMeta(metadata: TableMetadata, overrides?:
database: metadata.database,
columns: metadata.columns,
primaryKeys: metadata.primaryKeys,
physicalPrimaryKeys: physicalTablePrimaryKeys(metadata.columns, metadata.indexes),
};
}
@@ -234,6 +234,22 @@ describe("settingsTransfer", () => {
expect(result.value.editorSettings.snippets).toEqual(settings.snippets);
});
it("round-trips table default sorting settings", () => {
const settings = {
...DEFAULT_EDITOR_SETTINGS,
tableOpenSortMode: "database",
tableDatabaseSortDirection: "desc",
tableLocalSortDirection: "asc",
} as EditorSettings;
const result = parseSettingsTransferFile(serializeSettingsTransfer(settings));
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.editorSettings.tableOpenSortMode).toBe("database");
expect(result.value.editorSettings.tableDatabaseSortDirection).toBe("desc");
expect(result.value.editorSettings.tableLocalSortDirection).toBe("asc");
expect(transferCategoryForKey("tableOpenSortMode")).toBe("data");
});
it("rejects custom theme items that lack id or name", () => {
const validTheme = { ...DEFAULT_EDITOR_SETTINGS.customThemes[0] };
const idless = parseSettingsTransferFile(fileWith({ customThemes: [{ ...validTheme, id: "" }] }));
@@ -60,6 +60,9 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"dataGridCrosshairHighlight",
"pageSize",
"tableOpenPageSize",
"tableOpenSortMode",
"tableDatabaseSortDirection",
"tableLocalSortDirection",
"queryResultMaxRowsEnabled",
"queryResultMaxRows",
"externalSqlEditorMaxMb",
@@ -195,6 +195,9 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "navigation-disconnect-tabs", category: "navigation", titleKey: "settings.disconnectTabHandlingMode", descriptionKey: "settings.disconnectTabHandlingModeDescription", targetId: "navigation" },
{ id: "query-page-size", category: "data", titleKey: "settings.queryPageSize", descriptionKey: "settings.queryPageSizeDescription", targetId: "data" },
{ id: "data-page-size", category: "data", titleKey: "settings.tableOpenPageSize", descriptionKey: "settings.tableOpenPageSizeDescription", targetId: "data" },
{ id: "data-table-open-sort-mode", category: "data", titleKey: "settings.tableOpenSortMode", descriptionKey: "settings.tableOpenSortDescription", targetId: "data" },
{ id: "data-table-database-sort-direction", category: "data", titleKey: "settings.tableDatabaseSortDirection", targetId: "data" },
{ id: "data-table-local-sort-direction", category: "data", titleKey: "settings.tableLocalSortDirection", targetId: "data" },
{ id: "default-auto-keep-results", category: "data", titleKey: "settings.defaultAutoKeepResults", descriptionKey: "settings.defaultAutoKeepResultsDescription", targetId: "default-auto-keep-results" },
{ id: "multi-statement-default-view", category: "data", titleKey: "settings.multiStatementDefaultView", descriptionKey: "settings.multiStatementDefaultViewDescription", targetId: "multi-statement-default-view" },
{ id: "query-result-max-rows", category: "data", titleKey: "settings.queryResultMaxRows", descriptionKey: "settings.queryResultMaxRowsDescription", targetId: "data" },
@@ -153,6 +153,9 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record<SettingsTransferCategoryId, readon
"dataGridShowWhitespace",
"pageSize",
"tableOpenPageSize",
"tableOpenSortMode",
"tableDatabaseSortDirection",
"tableLocalSortDirection",
"queryResultMaxRowsEnabled",
"queryResultMaxRows",
"externalSqlEditorMaxMb",
@@ -0,0 +1,31 @@
import type { DatabaseType, QueryTab } from "@/types/database";
import type { EditorSettings } from "@/stores/settingsStore";
import { databaseSortSupportedForDatabase } from "@/lib/dataGrid/dataGridSort";
import { quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
export function resolveTableDefaultSort(settings: Pick<EditorSettings, "tableOpenSortMode" | "tableDatabaseSortDirection" | "tableLocalSortDirection">, databaseType: DatabaseType | undefined, physicalPrimaryKeys: string[], identifierQuote?: string) {
const mode = settings.tableOpenSortMode ?? "none";
const direction = (mode === "local" ? settings.tableLocalSortDirection : settings.tableDatabaseSortDirection) ?? "asc";
const columns = databaseType === "influxdb" || databaseType === "influxdb3" ? [] : physicalPrimaryKeys;
const orderBy = mode === "database" && columns.length && databaseSortSupportedForDatabase(databaseType) ? columns.map((column) => `${databaseType === "neo4j" ? "n." : ""}${quoteTableDataIdentifier(databaseType, column, identifierQuote)} ${direction.toUpperCase()}`).join(", ") : undefined;
return { mode, direction, columns, orderBy };
}
export function applyTableDefaultSortResult(tab: QueryTab, sort: ReturnType<typeof resolveTableDefaultSort>, sortLocally: (id: string, column: string, index: number, direction: "asc" | "desc") => void) {
if (!tab.result || tab.result.execution_error || tab.isExecuting) return;
const column = sort.columns[0];
const index = column ? tab.result.columns.indexOf(column) : -1;
if (!column || index < 0) return;
if (sort.orderBy) {
tab.orderByInput = sort.orderBy;
// A single-column grid indicator must not overwrite a composite ORDER BY.
if (sort.columns.length === 1) {
tab.resultSortColumn = column;
tab.resultSortColumnIndex = index;
tab.resultSortDirection = sort.direction;
tab.resultSortMode = "database";
}
} else if (sort.mode === "local" && !tab.result.large_value_cells?.some((cell) => cell.column_index === index)) {
sortLocally(tab.id, column, index, sort.direction);
}
}
@@ -29,6 +29,13 @@ export function editablePrimaryKeys(databaseType: DatabaseType | undefined, colu
return primaryKeys;
}
/** Physical primary keys only; unlike editable row identifiers, never fall back to unique indexes or synthetic keys. */
export function physicalTablePrimaryKeys(columns: readonly Pick<ColumnInfo, "name" | "is_primary_key">[], indexes: readonly Pick<IndexInfo, "columns" | "is_primary">[] = []): string[] {
const columnPrimaryKeys = columns.filter((column) => column.is_primary_key).map((column) => column.name);
if (columnPrimaryKeys.length > 0) return columnPrimaryKeys;
return indexes.find((index) => index.is_primary && index.columns.length > 0)?.columns ?? [];
}
export function editableRowIdentifierColumns(databaseType: DatabaseType | undefined, columns: ColumnInfo[], indexes?: IndexInfo[], tableType?: string): string[] {
const primaryKeys = editablePrimaryKeys(databaseType, columns, tableType);
const oracleRowIdFallback = getDatabaseCapability(databaseType).syntheticKey === "oracle-rowid" && primaryKeys.length === 1 && primaryKeys[0]?.toUpperCase() === DBX_ROWID_COLUMN;
+12
View File
@@ -793,6 +793,9 @@ export interface EditorSettings {
appLayout: "separated" | "classic";
pageSize: number;
tableOpenPageSize: number;
tableOpenSortMode: "none" | "database" | "local";
tableDatabaseSortDirection: "asc" | "desc";
tableLocalSortDirection: "asc" | "desc";
queryResultMaxRowsEnabled: boolean;
queryResultMaxRows: number;
externalSqlEditorMaxMb: number;
@@ -1038,6 +1041,9 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
appLayout: "classic",
pageSize: 100,
tableOpenPageSize: 100,
tableOpenSortMode: "none",
tableDatabaseSortDirection: "asc",
tableLocalSortDirection: "asc",
queryResultMaxRowsEnabled: true,
queryResultMaxRows: DEFAULT_QUERY_RESULT_MAX_ROWS,
externalSqlEditorMaxMb: 64,
@@ -1522,6 +1528,9 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
appLayout: settings.appLayout ?? DEFAULT_EDITOR_SETTINGS.appLayout,
pageSize: normalizeResultPageSize(settings.pageSize),
tableOpenPageSize: normalizeResultPageSize(settings.tableOpenPageSize, DEFAULT_EDITOR_SETTINGS.tableOpenPageSize),
tableOpenSortMode: settings.tableOpenSortMode === "database" || settings.tableOpenSortMode === "local" ? settings.tableOpenSortMode : "none",
tableDatabaseSortDirection: settings.tableDatabaseSortDirection === "desc" ? "desc" : "asc",
tableLocalSortDirection: settings.tableLocalSortDirection === "desc" ? "desc" : "asc",
queryResultMaxRowsEnabled: settings.queryResultMaxRowsEnabled !== false,
queryResultMaxRows: normalizeQueryResultMaxRows(settings.queryResultMaxRows),
externalSqlEditorMaxMb: normalizeExternalSqlEditorMaxMb(settings.externalSqlEditorMaxMb),
@@ -2289,6 +2298,9 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.appLayout !== undefined) editorSettings.value.appLayout = partial.appLayout;
if (partial.pageSize !== undefined) editorSettings.value.pageSize = normalizeResultPageSize(partial.pageSize);
if (partial.tableOpenPageSize !== undefined) editorSettings.value.tableOpenPageSize = normalizeResultPageSize(partial.tableOpenPageSize, DEFAULT_EDITOR_SETTINGS.tableOpenPageSize);
if (partial.tableOpenSortMode !== undefined) editorSettings.value.tableOpenSortMode = partial.tableOpenSortMode === "database" || partial.tableOpenSortMode === "local" ? partial.tableOpenSortMode : "none";
if (partial.tableDatabaseSortDirection !== undefined) editorSettings.value.tableDatabaseSortDirection = partial.tableDatabaseSortDirection === "desc" ? "desc" : "asc";
if (partial.tableLocalSortDirection !== undefined) editorSettings.value.tableLocalSortDirection = partial.tableLocalSortDirection === "desc" ? "desc" : "asc";
if (partial.queryResultMaxRowsEnabled !== undefined) editorSettings.value.queryResultMaxRowsEnabled = Boolean(partial.queryResultMaxRowsEnabled);
if (partial.queryResultMaxRows !== undefined) editorSettings.value.queryResultMaxRows = normalizeQueryResultMaxRows(partial.queryResultMaxRows, editorSettings.value.queryResultMaxRows);
if (partial.externalSqlEditorMaxMb !== undefined) editorSettings.value.externalSqlEditorMaxMb = normalizeExternalSqlEditorMaxMb(partial.externalSqlEditorMaxMb);
+2
View File
@@ -1789,6 +1789,8 @@ export interface QueryTab {
database?: string;
columns: ColumnInfo[];
primaryKeys: string[];
/** Physical primary keys used for table-open default sorting; excludes unique and synthetic row identifiers. */
physicalPrimaryKeys?: string[];
};
tableMetaUpdatedAt?: number;
/** 该 tab 的 tableMeta 是哪个连接元数据代次下写入的:disconnect / 关闭数据库 /