fix(oracle): preserve DATE time in display and export

This commit is contained in:
zipg authored and GitHub committed 2026-07-09 15:04:34 +08:00
1 parent aa0abcd670
commit ce8cb2b509
17 files changed
+504 -58

No files matched your search

+14 -2
View File
@@ -4024,6 +4024,11 @@ function tableColumnForGridColumn(columnIndex: number): ColumnInfo | undefined {
return props.tableMeta?.columns.find((column) => column.name.toLowerCase() === columnName.toLowerCase());
}
function resultColumnInfoForGridColumn(columnIndex: number): Pick<ColumnInfo, "data_type"> | undefined {
const dataType = props.result.column_types?.[columnIndex]?.trim();
return dataType ? { data_type: dataType } : undefined;
}
function temporalEditorKindForColumn(columnIndex: number): TemporalCellEditorKind | undefined {
return temporalCellEditorKind(tableColumnForGridColumn(columnIndex)?.data_type, props.databaseType);
}
@@ -5351,7 +5356,8 @@ function formatCell(value: CellValue, columnIndex?: number): string {
const formatter = columnIndex === undefined ? undefined : resolvedColumnFormatters.value[columnIndex];
const columnName = columnIndex === undefined ? undefined : props.result.columns[columnIndex];
const columnInfo = columnIndex === undefined ? undefined : tableColumnForGridColumn(columnIndex);
const arrayDisplay = formatter ? undefined : dataGridCellDisplayText({ value, databaseType: props.databaseType, columnInfo });
const displayColumnInfo = columnInfo ?? (columnIndex === undefined ? undefined : resultColumnInfoForGridColumn(columnIndex));
const arrayDisplay = formatter ? undefined : dataGridCellDisplayText({ value, databaseType: props.databaseType, columnInfo: displayColumnInfo });
if (arrayDisplay !== undefined) return arrayDisplay;
const binaryDisplay = formatter ? null : binaryCellDisplayText(value, columnInfo?.data_type ?? (columnName ? columnTypeMap.value.get(columnName) : undefined));
if (binaryDisplay) return binaryDisplay;
@@ -5392,7 +5398,13 @@ function draftCellPlaceholder(item: RowItem | undefined, columnIndex: number): s
return item?.isDraft && item.data[columnIndex] === null ? t("grid.quickEntryDraftPlaceholder") : null;
}
watch(() => [props.result.columns.join("\u0000"), columnFormatterSignatures.value.join("\u0000")], clearCellFormatCache);
function columnTypeCacheSignature(): string {
const resultTypes = props.result.column_types?.join("\u0000") ?? "";
const tableTypes = props.tableMeta?.columns?.map((column) => `${column.name}:${column.data_type}`).join("\u0000") ?? "";
return `${resultTypes}\u0001${tableTypes}`;
}
watch(() => [props.result.columns.join("\u0000"), columnFormatterSignatures.value.join("\u0000"), columnTypeCacheSignature()], clearCellFormatCache);
function quoteIdent(name: string): string {
return quoteTableIdentifier(props.databaseType, name);
@@ -237,6 +237,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
tableName: tableMeta.value?.tableName ?? null,
copyInsertTargetLabel: copyInsertTargetLabel?.value ?? null,
columns: columns.value,
columnTypes: columnTypes.value ?? null,
sourceColumns: sourceColumns.value ?? null,
excludePrimaryKeys,
insertMode,
@@ -311,6 +312,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
databaseType: databaseType.value,
tableMeta: tableMeta.value,
columns: columns.value,
columnTypes: columnTypes.value,
sourceColumns: sourceColumns.value,
rows: rows.map((item) => item.data),
excludePrimaryKeys,
@@ -1075,9 +1077,10 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
} {
const exportColumns = tableMeta.value ? effectiveColumns(sourceColumns.value, result.columns) : result.columns;
const columnIndexes = exportColumns.map((column, index) => ({ column, index })).filter((item): item is { column: string; index: number } => !!item.column);
const exportColumnTypes = columnTypes.value?.length === result.columns.length ? columnTypes.value : undefined;
return {
columns: columnIndexes.map((item) => item.column),
columnTypes: tableMeta.value ? columnIndexes.map((item) => columnTypes.value?.[item.index]) : undefined,
columnTypes: exportColumnTypes ? columnIndexes.map((item) => exportColumnTypes[item.index]) : undefined,
rows: result.rows.map((row) => columnIndexes.map((item) => row[item.index] ?? null)),
};
}
@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { dataGridCellDisplayText } from "@/lib/dataGrid/dataGridCellCoercion";
describe("dataGridCellDisplayText", () => {
it("formats Oracle DATE values without RFC3339 separators", () => {
expect(
dataGridCellDisplayText({
value: "2022-08-25T09:58:43Z",
databaseType: "oracle",
columnInfo: { data_type: "DATE" },
}),
).toBe("2022-08-25 09:58:43");
});
it("formats midnight Oracle DATE values as a date", () => {
expect(
dataGridCellDisplayText({
value: "2022-08-25T00:00:00Z",
databaseType: "oracle",
columnInfo: { data_type: "DATE" },
}),
).toBe("2022-08-25");
});
it("does not format non-date Oracle strings", () => {
expect(
dataGridCellDisplayText({
value: "2022-08-25T09:58:43Z",
databaseType: "oracle",
columnInfo: { data_type: "VARCHAR2(64)" },
}),
).toBeUndefined();
});
});
@@ -45,6 +45,9 @@ export function dataGridCellDisplayText(options: { value: GridCellValue; databas
if (Array.isArray(options.value) && options.databaseType === "postgres" && isPostgresArrayColumn(options.columnInfo, options.value)) {
return formatPostgresArrayText(options.value);
}
if (typeof options.value === "string" && isOracleDateColumn(options.databaseType, options.columnInfo)) {
return formatOracleDateDisplayText(options.value);
}
return undefined;
}
@@ -113,6 +116,39 @@ function normalizeDataType(dataType: string | undefined): string {
return (dataType ?? "").trim().toLowerCase();
}
function isOracleDateColumn(databaseType: DatabaseType | undefined, columnInfo: Pick<ColumnInfo, "data_type"> | undefined): boolean {
if (databaseType !== "oracle" && databaseType !== "oceanbase-oracle") return false;
const base = normalizeDataType(columnInfo?.data_type).split(/[()\s\t\n]/)[0] ?? "";
return base === "date";
}
function formatOracleDateDisplayText(value: string): string | undefined {
const parts = parseOracleDateLikeText(value);
if (!parts) return undefined;
if (parts.time === "00:00:00" && !parts.fraction) return parts.date;
return `${parts.date} ${parts.time}${parts.fraction ?? ""}`;
}
function parseOracleDateLikeText(value: string): { date: string; time: string; fraction?: string } | undefined {
if (!/^\d{4}-\d{2}-\d{2}/.test(value)) return undefined;
const date = value.slice(0, 10);
if (value.length === 10) return { date, time: "00:00:00" };
const separator = value[10];
if (separator !== "T" && separator !== " ") return undefined;
if (!/^\d{2}:\d{2}:\d{2}/.test(value.slice(11))) return undefined;
const time = value.slice(11, 19);
let rest = value.slice(19);
let fraction: string | undefined;
if (rest.startsWith(".")) {
const match = rest.match(/^(\.\d{1,9})(.*)$/);
if (!match) return undefined;
fraction = match[1];
rest = match[2];
}
if (rest && !/^z$/i.test(rest) && !/^[+-]\d{2}:\d{2}$/.test(rest)) return undefined;
return { date, time, fraction };
}
function isExactDecimalDataType(dataType: string): boolean {
return /\b(?:decimal|numeric|number|dec|money|smallmoney|bigdecimal|bignumeric|big_numeric|fixed)\b/.test(dataType);
}
@@ -45,6 +45,7 @@ export interface DataGridCopyInsertStatementOptions {
databaseType?: DatabaseType;
tableMeta?: DataGridTableMeta;
columns: string[];
columnTypes?: Array<string | null | undefined>;
sourceColumns?: Array<string | undefined>;
rows: GridCellValue[][];
excludePrimaryKeys?: boolean;
@@ -156,6 +156,7 @@ describe("queryStore database open state", () => {
vi.unstubAllGlobals();
const storage = installLocalStorage();
storage.set("dbx-editor-settings", JSON.stringify({ openTabsRestoreMode: "none" }));
storage.set("dbx-app-state:open_tabs", JSON.stringify({ tabs: JSON.parse(persistedTabs), activeTabId: "tab-1" }));
storage.set(OPEN_TABS_STORAGE_KEY, persistedTabs);
storage.set(ACTIVE_TAB_STORAGE_KEY, "tab-1");
setActivePinia(createPinia());
@@ -170,6 +171,7 @@ describe("queryStore database open state", () => {
expect(store.activeTabId).toBeNull();
expect(storage.get(OPEN_TABS_STORAGE_KEY)).toBeUndefined();
expect(storage.get(ACTIVE_TAB_STORAGE_KEY)).toBeUndefined();
expect(JSON.parse(storage.get("dbx-app-state:open_tabs") ?? "{}")).toEqual({ tabs: [], activeTabId: null });
});
it("restores only pinned tabs when launch restore mode is pinned", async () => {
+6
View File
@@ -681,6 +681,12 @@ export const useQueryStore = defineStore("query", () => {
if (saved?.tabs && Array.isArray(saved.tabs)) {
const restored = restoreSavedTabsFromPayload(saved);
applyRestoredOpenTabs(restored);
if (useSettingsStore().editorSettings.openTabsRestoreMode === "none") {
// Restore is explicitly disabled, so stale saved payloads should not
// reappear if the user later changes the setting.
clearLegacySavedTabs();
await saveTabs(tabs.value, activeTabId.value).catch(() => undefined);
}
isOpenTabsLoaded.value = true;
return;
}