mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 08:13:34 +00:00
feat(editor): configurable default sort when opening tables
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -6529,6 +6529,15 @@ export default withEnglishFallback({
|
||||
syncSuccess: "データ同期が正常に実行されました",
|
||||
},
|
||||
settings: {
|
||||
tableOpenSortMode: "テーブルの既定の並び順",
|
||||
tableDatabaseSortDirection: "データベースの既定のソート方向",
|
||||
tableLocalSortDirection: "現在のページの既定のソート方向",
|
||||
tableOpenSortDescription: "新しく開くテーブルに適用します。データベースでは主キー全体、ページ内では最初の主キー列で読み込み済みの行を並べ替えます。利用できない場合は従来の順序を維持します。",
|
||||
tableSortUnchanged: "現在の動作を維持",
|
||||
tableSortDatabase: "データベース全体でソート",
|
||||
tableSortLocal: "現在のページでソート",
|
||||
tableSortAscending: "昇順 ASC",
|
||||
tableSortDescending: "降順 DESC",
|
||||
title: "設定",
|
||||
searchSettings: "設定を検索",
|
||||
searchSettingsNoResults: "一致する設定がありません",
|
||||
|
||||
@@ -6241,6 +6241,15 @@ export default withEnglishFallback({
|
||||
syncSuccess: "데이터 동기화를 성공적으로 실행했습니다",
|
||||
},
|
||||
settings: {
|
||||
tableOpenSortMode: "테이블 기본 정렬",
|
||||
tableDatabaseSortDirection: "데이터베이스 기본 정렬 방향",
|
||||
tableLocalSortDirection: "현재 페이지 기본 정렬 방향",
|
||||
tableOpenSortDescription: "새 테이블에 적용합니다. 데이터베이스 정렬은 전체 기본 키를, 로컬 정렬은 첫 번째 기본 키 열과 로드된 행만 사용합니다. 지원하지 않으면 기존 순서를 유지합니다.",
|
||||
tableSortUnchanged: "기존 동작 유지",
|
||||
tableSortDatabase: "데이터베이스 정렬",
|
||||
tableSortLocal: "현재 페이지 정렬",
|
||||
tableSortAscending: "오름차순 ASC",
|
||||
tableSortDescending: "내림차순 DESC",
|
||||
title: "설정",
|
||||
searchSettings: "설정 검색",
|
||||
searchSettingsNoResults: "일치하는 설정이 없습니다",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -6904,6 +6904,15 @@ export default withEnglishFallback({
|
||||
syncSuccess: "数据同步执行成功",
|
||||
},
|
||||
settings: {
|
||||
tableOpenSortMode: "打开表默认排序",
|
||||
tableDatabaseSortDirection: "全表默认排序方向",
|
||||
tableLocalSortDirection: "当前页默认排序方向",
|
||||
tableOpenSortDescription: "仅影响新打开的表。全表按完整主键排序,当前页按第一个主键字段排序且仅处理已加载数据;无主键或不支持时保持原行为。",
|
||||
tableSortUnchanged: "保持现有行为",
|
||||
tableSortDatabase: "全表排序",
|
||||
tableSortLocal: "当前页排序",
|
||||
tableSortAscending: "升序 ASC",
|
||||
tableSortDescending: "降序 DESC",
|
||||
title: "设置",
|
||||
searchSettings: "搜索设置",
|
||||
searchSettingsNoResults: "未找到匹配的设置项",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 / 关闭数据库 /
|
||||
|
||||
Reference in New Issue
Block a user