🐛 fix(data-grid): 修复 MySQL bit 字段复制 INSERT 引号错误

- 识别 MySQL 家族 bit 列并生成未加引号的数值字面量
- 兼容布尔、十进制、十六进制和二进制值格式
- 增加复制 INSERT 回归测试并保留普通字段序列化行为

Fixes #786
This commit is contained in:
Syngnat
2026-08-03 10:46:27 +08:00
parent b7774f8d75
commit 573df673b1
2 changed files with 106 additions and 1 deletions

View File

@@ -78,6 +78,50 @@ describe('buildCopyInsertSQL', () => {
expect(sql).toBe("INSERT INTO `users` (`name`, `id`) VALUES ('Ada', '7');");
});
it('emits unquoted MySQL bit literals while keeping ordinary values quoted', () => {
const sql = buildCopyInsertSQL({
dbType: 'mysql',
tableName: 'flags',
orderedCols: ['disabled', 'enabled', 'mask', 'note'],
record: {
disabled: 0,
enabled: 1,
mask: '0x05',
note: '1',
},
columnTypesByLowerName: {
disabled: 'bit(1)',
enabled: 'bit(1)',
mask: 'bit(4)',
note: 'varchar(16)',
},
});
expect(sql).toBe("INSERT INTO `flags` (`disabled`, `enabled`, `mask`, `note`) VALUES (0, 1, 5, '1');");
});
it('normalizes MySQL bit binary and decimal text values', () => {
const sql = buildCopyInsertSQL({
dbType: 'mysql',
tableName: 'flags',
orderedCols: ['zero', 'binary_value', 'large_value'],
record: {
zero: '0',
binary_value: "b'101'",
large_value: '18446744073709551615',
},
columnTypesByLowerName: {
zero: 'BIT',
binary_value: 'bit(8)',
large_value: 'bit(64)',
},
});
expect(sql).toBe(
"INSERT INTO `flags` (`zero`, `binary_value`, `large_value`) VALUES (0, 5, 18446744073709551615);",
);
});
it('keeps RFC3339-looking text unchanged for non-temporal columns', () => {
const sql = buildCopyInsertSQL({
dbType: 'postgres',

View File

@@ -1,6 +1,6 @@
import type { IndexDefinition } from '../types';
import { escapeLiteral, quoteIdentPart, quoteQualifiedIdent } from '../utils/sql';
import { isOracleLikeDialect } from '../utils/sqlDialect';
import { isMysqlFamilyDialect, isOracleLikeDialect } from '../utils/sqlDialect';
type BuildCopyInsertSQLParams = {
dbType: string;
@@ -212,10 +212,71 @@ const formatOracleTemporalLiteral = (value: any, columnType?: string): string |
return `TO_DATE('${escaped}', 'YYYY-MM-DD HH24:MI:SS')`;
};
const isMySQLBitColumnType = (columnType?: string): boolean => {
const normalized = String(columnType || '').trim().toLowerCase();
return /^bit(?:\s*\(\s*\d+\s*\))?$/.test(normalized);
};
const formatMySQLBitLiteral = (value: any): string | null => {
if (typeof value === 'boolean') {
return value ? '1' : '0';
}
if (typeof value === 'number') {
if (!Number.isSafeInteger(value) || value < 0) return null;
return BigInt(value).toString(10);
}
if (typeof value === 'bigint') {
if (value < 0n) return null;
return value.toString(10);
}
if (typeof value !== 'string') return null;
const raw = value.trim();
if (!raw) return null;
const binaryLiteral = raw.match(/^[bB]['"]([01]+)['"]$/);
if (binaryLiteral) {
try {
return BigInt(`0b${binaryLiteral[1]}`).toString(10);
} catch {
return null;
}
}
let numericText = raw;
let radix = 10;
if (/^0[bB][01]+$/.test(raw)) {
numericText = raw.slice(2);
radix = 2;
} else if (/^0[xX][0-9a-f]+$/.test(raw)) {
numericText = raw.slice(2);
radix = 16;
} else if (!/^\d+$/.test(raw)) {
if (/^(true|false)$/i.test(raw)) {
return raw.toLowerCase() === 'true' ? '1' : '0';
}
return null;
}
try {
return BigInt(radix === 10 ? raw : `${radix === 2 ? '0b' : '0x'}${numericText}`).toString(10);
} catch {
return null;
}
};
const formatCopySqlLiteral = (value: any, columnType?: string, dbType = ''): string => {
if (value === null || value === undefined) {
return 'NULL';
}
if (isMysqlFamilyDialect(dbType) && isMySQLBitColumnType(columnType)) {
const mysqlBitLiteral = formatMySQLBitLiteral(value);
if (mysqlBitLiteral) {
return mysqlBitLiteral;
}
}
if (isOracleLikeDialect(dbType)) {
const oracleTemporalLiteral = formatOracleTemporalLiteral(value, columnType);
if (oracleTemporalLiteral) {