feat(grid): preview text stored in MySQL BLOB cells

This commit is contained in:
zipg authored and GitHub committed 2026-08-24 20:34:04 +08:00
1 parent 566bc87014
commit f45fcdb69d
8 files changed
+217 -15

No files matched your search

@@ -114,6 +114,7 @@ const HEX_ESCAPE_RE = /^(?:\\x[0-9a-fA-F]{2}|\s)+$/;
const BINARY_TYPE_RE = /^(?:blob|tinyblob|mediumblob|longblob|bytea|bytes|binary|varbinary|image|raw|long\s+raw)(?:\b|\()/i;
const FIXED_BINARY_TYPE_RE = /^binary(?:\b|\()/i;
const BINARY_STRING_TYPE_RE = /^(?:binary|varbinary)(?:\b|\()/i;
const BLOB_TYPE_RE = /^(?:blob|tinyblob|mediumblob|longblob)(?:\b|\()/i;
const MYSQL_FILE_IMPORT_TYPE_RE = /^(?:blob|tinyblob|mediumblob|longblob|binary|varbinary)(?:\b|\()/i;
function copyBytesForBlob(bytes: Uint8Array): Uint8Array<ArrayBuffer> {
@@ -182,6 +183,11 @@ export function isBinaryCellColumnType(columnType?: string): boolean {
return !!type && BINARY_TYPE_RE.test(type);
}
export function isBlobCellColumnType(columnType?: string): boolean {
const type = (columnType ?? "").trim();
return !!type && BLOB_TYPE_RE.test(type);
}
export function canImportBinaryCellFile(databaseType?: DatabaseType, columnType?: string): boolean {
const type = (columnType ?? "").trim();
if (databaseType === "postgres") return /^bytea(?:\b|\()/i.test(type);
@@ -200,14 +206,34 @@ export function binaryCellDisplayText(value: unknown, columnType?: string, origi
}
const bytes = parseBinaryCellBytes(value, columnType, databaseType);
if (!bytes || !isBinaryCellColumnType(columnType)) return null;
if (BINARY_STRING_TYPE_RE.test((columnType ?? "").trim())) {
const previewBytes = FIXED_BINARY_TYPE_RE.test((columnType ?? "").trim()) ? trimTrailingNullBytes(bytes) : bytes;
const text = printableUtf8Text(previewBytes);
if (isBinaryCellTextPreviewColumn(columnType, databaseType)) {
const text = binaryCellUtf8TextBytes(bytes, columnType);
if (text !== null) return text;
}
return `${binaryCellDisplayLabel(columnType)} [${formatBinaryCellByteSize(bytes.length)}]`;
}
export function binaryCellUtf8Text(value: unknown, columnType?: string, databaseType?: DatabaseType): string | null {
if (!isBinaryCellColumnType(columnType) || !isBinaryCellTextPreviewColumn(columnType, databaseType)) return null;
const bytes = parseBinaryCellBytes(value, columnType, databaseType);
if (!bytes) return null;
return binaryCellUtf8TextBytes(bytes, columnType);
}
// MySQL BLOB 的文本预览必须与编辑写回路径(coerceMysqlBlobTextValue,仅 mysql)走同一闸门:
// SQLite/DuckDB/H2/Firebird 等库同样暴露 `blob` 列,若一律按文本预览会出现
// “显示是文本、编辑器却是十六进制”的不一致,故 blob 文本预览仅在 mysql 连接开启。
// binary/varbinary 的文本预览早于该特性存在(如 TDengine BINARY 文本),保持全库通用。
function isBinaryCellTextPreviewColumn(columnType: string | undefined, databaseType: DatabaseType | undefined): boolean {
if (BINARY_STRING_TYPE_RE.test((columnType ?? "").trim())) return true;
return isBlobCellColumnType(columnType) && databaseType === "mysql";
}
function binaryCellUtf8TextBytes(bytes: Uint8Array, columnType?: string): string | null {
const previewBytes = FIXED_BINARY_TYPE_RE.test((columnType ?? "").trim()) ? trimTrailingNullBytes(bytes) : bytes;
return printableUtf8Text(previewBytes);
}
function trimTrailingNullBytes(bytes: Uint8Array): Uint8Array {
let end = bytes.length;
while (end > 0 && bytes[end - 1] === 0) end--;
@@ -1,5 +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 { isNumericColumnType } from "@/lib/dataGrid/dataGridColumnType";
import { isBooleanColumnType } from "@/lib/dataGrid/dataGridBooleanColumn";
@@ -17,6 +18,8 @@ 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);
if (blobValue !== undefined) return blobValue;
const postgresArrayValue = coercePostgresArrayValue(options);
if (postgresArrayValue !== undefined) return postgresArrayValue;
// Excel-pasted values often carry thousands separators (10,000.00) that make
@@ -68,12 +71,24 @@ 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)) {
const text = binaryCellUtf8Text(value, options.columnInfo?.data_type, options.databaseType);
if (text !== null) return text;
}
if (Array.isArray(value) && options.databaseType === "postgres" && isPostgresArrayColumn(options.columnInfo, value)) {
return formatPostgresArrayText(value);
}
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;
const originalText = binaryCellUtf8Text(options.oldValue, options.columnInfo?.data_type, options.databaseType);
if (originalText === null) return undefined;
if (options.value === originalText) return options.oldValue;
return binaryCellBytesToHexValue(new TextEncoder().encode(options.value));
}
export function dataGridCellDisplayText(options: { value: GridCellValue; databaseType: DatabaseType | undefined; columnInfo: Pick<ColumnInfo, "data_type"> | undefined }): string | undefined {
if (Array.isArray(options.value) && options.databaseType === "postgres" && isPostgresArrayColumn(options.columnInfo, options.value)) {
return formatPostgresArrayText(options.value);