feat(editor): configurable default sort when opening tables
This commit is contained in:
25 files changed
+417
-23
No files matched your search
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user