fix(grid): preview text stored in Oracle BLOB cells

This commit is contained in:
lxk955
2026-10-11 13:34:50 +08:00
committed by GitHub
parent 2826bb5fc5
commit 03a2cd1bd7
7 changed files with 95 additions and 11 deletions
@@ -30,7 +30,7 @@ const props = defineProps<{
downloadBinaryValue: (detail: DataGridCellDetail | null, mode: BinaryCellDownloadMode) => void | Promise<void>;
canImportBinaryValue: (detail: DataGridCellDetail | null) => boolean;
importBinaryValue: (detail: DataGridCellDetail | null) => void | Promise<void>;
/** BLOB 文本预览与编辑写回一致,仅在 MySQL 连接开启。 */
/** BLOB 文本预览与编辑写回一致,在 MySQL / Oracle(含 OceanBase-Oracle)连接开启。 */
databaseType?: DatabaseType;
}>();
@@ -39,7 +39,7 @@ const props = defineProps<{
importBinaryValue: (detail: DataGridCellDetail | null) => void | Promise<void>;
openImagePreview: (src: string, title: string) => void;
canCopySqlCondition: () => boolean;
/** BLOB 文本预览与编辑写回一致,仅在 MySQL 连接开启。 */
/** BLOB 文本预览与编辑写回一致,在 MySQL / Oracle(含 OceanBase-Oracle)连接开启。 */
databaseType?: DatabaseType;
}>();
@@ -2091,6 +2091,57 @@ describe("cell detail surfaces", () => {
expect(copyText).toHaveBeenCalledWith("#2005805");
});
it("presents printable Oracle BLOB bytes as text and copies the presented value", async () => {
const copyText = vi.fn();
const blobDetail = detail({
type: "BLOB",
value: "0x48656c6c6f",
rawValue: "0x48656c6c6f",
rawValuePreview: "0x48656c6c6f",
displayValue: "Hello",
displayValuePreview: "Hello",
formattedJson: "",
});
const dialog = mountComponent(DataGridCellDetailDialog, {
open: true,
detail: blobDetail,
typeColorClass: () => "",
openImagePreview: vi.fn(),
copyText,
canDownloadBinaryValue: () => true,
downloadBinaryValue: vi.fn(),
canImportBinaryValue: () => false,
importBinaryValue: vi.fn(),
databaseType: "oracle",
});
const panel = mountComponent(DataGridCellDetailPanel, {
detail: blobDetail,
panelIsBottom: true,
metadataCollapsed: false,
valueFillsHeight: false,
editing: false,
sideJsonView: false,
showCompactJson: false,
canCompactJson: false,
typeColorClass: () => "",
canDownloadBinaryValue: () => true,
downloadBinaryValue: vi.fn(),
canImportBinaryValue: () => false,
importBinaryValue: vi.fn(),
openImagePreview: vi.fn(),
canCopySqlCondition: () => true,
databaseType: "oracle",
});
expect(hostText(dialog.root)).toContain("Hello");
expect(hostText(panel.root)).toContain("Hello");
dispatch(
findOne(dialog.root, (node) => node.props.title === "grid.copyValue"),
"click",
);
expect(copyText).toHaveBeenCalledWith("Hello");
});
it("keeps non-MySQL BLOB detail previews in hex", () => {
const panel = mountComponent(DataGridCellDetailPanel, {
detail: detail({ type: "BLOB", value: "0x2332303035383035", rawValue: "0x2332303035383035", rawValuePreview: "0x2332303035383035", displayValue: "BLOB [8 bytes]", displayValuePreview: "BLOB [8 bytes]", formattedJson: "" }),
@@ -306,14 +306,18 @@ export function binaryCellClipboardText(value: unknown, columnType?: string, dat
return printableGbkText(binaryPreviewBytes(bytes, columnType));
}
// MySQL BLOB 的文本预览必须与编辑写回路径(coerceMysqlBlobTextValue,仅 mysql)走同一闸门:
export function isBlobTextPreviewDatabase(databaseType?: DatabaseType): boolean {
return databaseType === "mysql" || databaseType === "oracle" || databaseType === "oceanbase-oracle";
}
// MySQL 与 Oracle(含 OceanBase-Oracle)BLOB 的文本预览必须与编辑写回路径(coerceBlobTextValue)走同一闸门:
// SQLite/DuckDB/H2/Firebird 等库同样暴露 `blob` 列,若一律按文本预览会出现
// “显示是文本、编辑器却是十六进制”的不一致,故 blob 文本预览仅在 mysql 连接开启。
// “显示是文本、编辑器却是十六进制”的不一致,故 blob 文本预览仅在支持的连接开启。
// binary/varbinary 的文本预览早于该特性存在(如 TDengine BINARY 文本),保持全库通用。
function isBinaryCellTextPreviewColumn(columnType: string | undefined, databaseType: DatabaseType | undefined): boolean {
if (isOpaqueAggregateStateColumnType(columnType)) return false;
if (BINARY_STRING_TYPE_RE.test((columnType ?? "").trim())) return true;
return isBlobCellColumnType(columnType) && databaseType === "mysql";
return isBlobCellColumnType(columnType) && isBlobTextPreviewDatabase(databaseType);
}
// 显示/复制路径的解码链:先严格 UTF-8,仅 MySQL 连接的 binary/varbinary 再回退严格
@@ -1,6 +1,6 @@
import type { GridCellValue } from "@/lib/dataGrid/dataGridSql";
import type { DatabaseType, ColumnInfo } from "@/types/database";
import { binaryCellBytesToHexValue, binaryCellUtf8Text, isBlobCellColumnType } from "@/lib/dataGrid/binaryCellDownload";
import { binaryCellBytesToHexValue, binaryCellUtf8Text, isBlobCellColumnType, isBlobTextPreviewDatabase } from "@/lib/dataGrid/binaryCellDownload";
import { isNumericColumnType } from "@/lib/dataGrid/dataGridColumnType";
import { isBooleanColumnType } from "@/lib/dataGrid/dataGridBooleanColumn";
@@ -26,7 +26,7 @@ export function coerceDataGridCellValue(options: CoerceDataGridCellValueOptions)
const { value, oldValue } = options;
if (value === "" && options.emptyStringAsNull) return null;
if (value === "" && oldValue === null && !options.preserveEmptyString) return null;
const blobValue = coerceMysqlBlobTextValue(options);
const blobValue = coerceBlobTextValue(options);
if (blobValue !== undefined) return blobValue;
const postgresArrayValue = coercePostgresArrayValue(options);
if (postgresArrayValue !== undefined) return postgresArrayValue;
@@ -82,7 +82,7 @@ function parseBooleanInput(value: string, allowNumericAliases: boolean): boolean
export function dataGridCellEditorText(options: { value: GridCellValue | undefined; databaseType: DatabaseType | undefined; columnInfo: Pick<ColumnInfo, "data_type"> | undefined }): string {
const value = options.value ?? null;
if (value === null) return "";
if (options.databaseType === "mysql" && isBlobCellColumnType(options.columnInfo?.data_type)) {
if (isBlobTextPreviewDatabase(options.databaseType) && isBlobCellColumnType(options.columnInfo?.data_type)) {
const text = binaryCellUtf8Text(value, options.columnInfo?.data_type, options.databaseType);
if (text !== null) return text;
}
@@ -92,8 +92,8 @@ export function dataGridCellEditorText(options: { value: GridCellValue | undefin
return typeof value === "object" ? JSON.stringify(value) : String(value);
}
function coerceMysqlBlobTextValue(options: CoerceDataGridCellValueOptions): GridCellValue | undefined {
if (options.databaseType !== "mysql" || !isBlobCellColumnType(options.columnInfo?.data_type)) return undefined;
function coerceBlobTextValue(options: CoerceDataGridCellValueOptions): GridCellValue | undefined {
if (!isBlobTextPreviewDatabase(options.databaseType) || !isBlobCellColumnType(options.columnInfo?.data_type)) return undefined;
const originalText = binaryCellUtf8Text(options.oldValue, options.columnInfo?.data_type, options.databaseType);
if (originalText === null) return undefined;
if (options.value === originalText) return options.oldValue;
+17 -1
View File
@@ -13,6 +13,7 @@ import {
isBinaryCellColumnType,
binaryCellUtf8Text,
isBlobCellColumnType,
isBlobTextPreviewDatabase,
MAX_BINARY_CELL_IMPORT_BYTES,
parseBinaryCellBytes,
parseBinaryCellHexValue,
@@ -246,7 +247,14 @@ test("binaryCellClipboardText decodes textual MySQL varbinary and preserves arbi
// 回归:SQLite/DuckDB 等库同样有 `blob` 列,文本预览必须与编辑写回路径一样仅对 mysql 开启,
// 否则出现“单元格/详情显示文本、编辑器却是十六进制”的不一致。
test("BLOB text preview stays limited to MySQL connections", () => {
test("BLOB text preview stays limited to MySQL and Oracle family connections", () => {
assert.equal(isBlobTextPreviewDatabase("mysql"), true);
assert.equal(isBlobTextPreviewDatabase("oracle"), true);
assert.equal(isBlobTextPreviewDatabase("oceanbase-oracle"), true);
assert.equal(isBlobTextPreviewDatabase("sqlite"), false);
assert.equal(isBlobTextPreviewDatabase("duckdb"), false);
assert.equal(isBlobTextPreviewDatabase(undefined), false);
assert.equal(binaryCellUtf8Text("0x2332303035383035", "LONGBLOB", "sqlite"), null);
assert.equal(binaryCellUtf8Text("0x2332303035383035", "BLOB", "duckdb"), null);
assert.equal(binaryCellUtf8Text("0x2332303035383035", "LONGBLOB", undefined), null);
@@ -254,6 +262,14 @@ test("BLOB text preview stays limited to MySQL connections", () => {
assert.equal(binaryCellDisplayText("0x2332303035383035", "BLOB", undefined, "duckdb"), "BLOB [8 bytes]");
assert.equal(binaryCellDisplayText("0x2332303035383035", "LONGBLOB", undefined), "BLOB [8 bytes]");
assert.equal(binaryCellDisplayText("0x2332303035383035", "LONGBLOB", undefined, "mysql"), "#2005805");
// Oracle 及 OceanBase-Oracle 的 BLOB 文本预览
assert.equal(binaryCellUtf8Text("0x48656c6c6f", "BLOB", "oracle"), "Hello");
assert.equal(binaryCellUtf8Text("0x48656c6c6f", "BLOB", "oceanbase-oracle"), "Hello");
assert.equal(binaryCellDisplayText("0x48656c6c6f", "BLOB", undefined, "oracle"), "Hello");
assert.equal(binaryCellDisplayText("0x48656c6c6f", "BLOB", undefined, "oceanbase-oracle"), "Hello");
assert.equal(binaryCellDisplayText("0x89504e47", "BLOB", undefined, "oracle"), "BLOB [4 bytes]");
// binary/varbinary 的文本预览是既有行为(如 TDengine BINARY 文本),不受 mysql 闸门影响。
assert.equal(binaryCellDisplayText("0x534e2d4130303031", "VARBINARY(8)", undefined, "sqlite"), "SN-A0001");
});
+13
View File
@@ -210,3 +210,16 @@ test("MySQL binary BLOB editor keeps non-text bytes in hex", () => {
assert.equal(dataGridCellEditorText({ value: oldValue, databaseType: "mysql", columnInfo }), oldValue);
assert.equal(coerceDataGridCellValue({ value: oldValue, oldValue, databaseType: "mysql", columnInfo }), oldValue);
});
test("Oracle and OceanBase-Oracle text BLOB editor decodes UTF-8 and writes edited bytes as hex", () => {
const oldValue = "0x2332303035383035";
const columnInfo = { data_type: "blob" };
for (const databaseType of ["oracle", "oceanbase-oracle"] as const) {
assert.equal(dataGridCellEditorText({ value: oldValue, databaseType, columnInfo }), "#2005805");
assert.strictEqual(coerceDataGridCellValue({ value: "#2005805", oldValue, databaseType, columnInfo }), oldValue);
assert.equal(coerceDataGridCellValue({ value: "新表达式", oldValue, databaseType, columnInfo }), "0xe696b0e8a1a8e8bebee5bc8f");
assert.equal(coerceDataGridCellValue({ value: "", oldValue, databaseType, columnInfo }), "0x");
}
});