mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(grid): preview text stored in Oracle BLOB cells
This commit is contained in:
@@ -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
@@ -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
@@ -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");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user