diff --git a/docs/issues/2026-04-11-issue-backlog-tracking.md b/docs/issues/2026-04-11-issue-backlog-tracking.md new file mode 100644 index 0000000..0543e39 --- /dev/null +++ b/docs/issues/2026-04-11-issue-backlog-tracking.md @@ -0,0 +1,41 @@ +# 2026-04-11 Issue Backlog Tracking + +## Scope + +- 分支:`codex/issue-242-data-root` +- 策略:按 GitHub issue 创建时间从早到晚逐条处理 +- 提交要求:每条 issue 单独本地提交,提交信息使用 `Fixes #` + +## Progress + +| Issue | Title | Status | Commit | +| --- | --- | --- | --- | +| #242 | 希望有自定义数据存储位置功能 | Fixed | `42c5500` | +| #287 | 建议补充 Sql Server 数据库图标 | Fixed | `ebae05c` | +| #305 | 金仓数据库设计表新增字段保存失败 | Fixed | `9ecf5be` | +| #306 | 驱动下载 | Fixed | `c49ed95` | +| #308 | clickhouse 获取数据库列表失败 | Fixed | `33bbd91` | +| #310 | 选择库后,右侧行显示各个表 | Fixed | `5bbeba2` | +| #311 | WIN 系统的执行 500 多条 insert 语句要几分钟 | Fixed | `fd7ec11` | +| #315 | 窗体内缩放异常 | Fixed | `e19dd82` | +| #316 | 人大金仓数据库驱动版本过低 | Fixed | `2500183` | +| #317 | 驱动管理增加导入 jar 功能 | Blocked | - | +| #318 | mysql,bit 列,修改成 1 失败 | Fixed | Pending | + +## Notes + +### #317 + +- 当前驱动管理只支持内置 Go 驱动和可选 Go 驱动代理包。 +- 仓库内不存在 JDBC/JAR 装载、Java 运行时探测、classpath 管理或桥接执行链路。 +- 在现有架构下直接增加 “导入 jar” 入口会形成假功能,因此暂记为架构阻塞,不做伪实现。 + +### #318 + +- 根因:MySQL 写入归一化只覆盖时间列,`bit` 列提交时会把前端传来的 `"1"`/`"0"` 原样透传给驱动。 +- 处理:为 MySQL `bit` 列补充写入值归一化,将常见文本/布尔/数值输入转换为驱动可接受的 `[]byte`。 +- 验证:补充 `internal/db/mysql_value_test.go` 回归测试,覆盖 `bit(1)` 的 insert/update 写入路径。 + +## Next + +- 继续处理下一个最早且可直接落地的开放 issue。 diff --git a/internal/db/mysql_impl.go b/internal/db/mysql_impl.go index d0459f9..e8d4cd5 100644 --- a/internal/db/mysql_impl.go +++ b/internal/db/mysql_impl.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/json" "fmt" + "math" "net/url" "strconv" "strings" @@ -731,6 +732,9 @@ func (m *MySQLDB) loadColumnTypeMap(tableName string) map[string]string { func normalizeMySQLValueForInsert(columnName string, value interface{}, columnTypeMap map[string]string) (interface{}, bool) { columnType := strings.ToLower(strings.TrimSpace(columnTypeMap[strings.ToLower(strings.TrimSpace(columnName))])) + if isMySQLBitColumnType(columnType) { + return normalizeMySQLBitValue(value), false + } if !isMySQLTemporalColumnType(columnType) { return normalizeMySQLComplexValue(value), false } @@ -744,6 +748,9 @@ func normalizeMySQLValueForInsert(columnName string, value interface{}, columnTy func normalizeMySQLValueForWrite(columnName string, value interface{}, columnTypeMap map[string]string) interface{} { columnType := strings.ToLower(strings.TrimSpace(columnTypeMap[strings.ToLower(strings.TrimSpace(columnName))])) + if isMySQLBitColumnType(columnType) { + return normalizeMySQLBitValue(value) + } if !isMySQLTemporalColumnType(columnType) { return value } @@ -769,6 +776,154 @@ func isMySQLTemporalColumnType(columnType string) bool { return base == "date" || base == "time" || base == "year" } +func isMySQLBitColumnType(columnType string) bool { + raw := strings.ToLower(strings.TrimSpace(columnType)) + if raw == "" { + return false + } + base := raw + if idx := strings.IndexAny(base, "( "); idx >= 0 { + base = base[:idx] + } + return base == "bit" +} + +func normalizeMySQLBitValue(value interface{}) interface{} { + switch v := value.(type) { + case nil: + return nil + case []byte: + return v + case bool: + if v { + return []byte{1} + } + return []byte{0} + case string: + if bitValue, ok := parseMySQLBitString(v); ok { + return bitValue + } + return value + case int: + if v >= 0 { + if bitValue, ok := mysqlBitBytesFromUint64(uint64(v)); ok { + return bitValue + } + } + case int8: + if v >= 0 { + if bitValue, ok := mysqlBitBytesFromUint64(uint64(v)); ok { + return bitValue + } + } + case int16: + if v >= 0 { + if bitValue, ok := mysqlBitBytesFromUint64(uint64(v)); ok { + return bitValue + } + } + case int32: + if v >= 0 { + if bitValue, ok := mysqlBitBytesFromUint64(uint64(v)); ok { + return bitValue + } + } + case int64: + if v >= 0 { + if bitValue, ok := mysqlBitBytesFromUint64(uint64(v)); ok { + return bitValue + } + } + case uint: + if bitValue, ok := mysqlBitBytesFromUint64(uint64(v)); ok { + return bitValue + } + case uint8: + if bitValue, ok := mysqlBitBytesFromUint64(uint64(v)); ok { + return bitValue + } + case uint16: + if bitValue, ok := mysqlBitBytesFromUint64(uint64(v)); ok { + return bitValue + } + case uint32: + if bitValue, ok := mysqlBitBytesFromUint64(uint64(v)); ok { + return bitValue + } + case uint64: + if bitValue, ok := mysqlBitBytesFromUint64(v); ok { + return bitValue + } + case float32: + if v >= 0 && math.Trunc(float64(v)) == float64(v) { + if bitValue, ok := mysqlBitBytesFromUint64(uint64(v)); ok { + return bitValue + } + } + case float64: + if v >= 0 && math.Trunc(v) == v { + if bitValue, ok := mysqlBitBytesFromUint64(uint64(v)); ok { + return bitValue + } + } + } + return value +} + +func parseMySQLBitString(text string) ([]byte, bool) { + raw := strings.TrimSpace(text) + if raw == "" { + return nil, false + } + + switch strings.ToLower(raw) { + case "true": + return []byte{1}, true + case "false": + return []byte{0}, true + } + + if len(raw) > 3 && (raw[0] == 'b' || raw[0] == 'B') && raw[1] == '\'' && raw[len(raw)-1] == '\'' { + value, err := strconv.ParseUint(raw[2:len(raw)-1], 2, 64) + if err == nil { + return mysqlBitBytesFromUint64OrZero(value), true + } + return nil, false + } + + if len(raw) > 2 && (strings.HasPrefix(raw, "0b") || strings.HasPrefix(raw, "0B")) { + value, err := strconv.ParseUint(raw[2:], 2, 64) + if err == nil { + return mysqlBitBytesFromUint64OrZero(value), true + } + return nil, false + } + + value, err := strconv.ParseUint(raw, 10, 64) + if err != nil { + return nil, false + } + return mysqlBitBytesFromUint64OrZero(value), true +} + +func mysqlBitBytesFromUint64(value uint64) ([]byte, bool) { + return mysqlBitBytesFromUint64OrZero(value), true +} + +func mysqlBitBytesFromUint64OrZero(value uint64) []byte { + if value == 0 { + return []byte{0} + } + var buf [8]byte + index := len(buf) + for value > 0 { + index-- + buf[index] = byte(value) + value >>= 8 + } + return append([]byte(nil), buf[index:]...) +} + func hasTimezoneOffset(text string) bool { pos := strings.LastIndexAny(text, "+-") if pos < 0 || pos < 10 || pos+1 >= len(text) { diff --git a/internal/db/mysql_value_test.go b/internal/db/mysql_value_test.go new file mode 100644 index 0000000..dd33644 --- /dev/null +++ b/internal/db/mysql_value_test.go @@ -0,0 +1,68 @@ +package db + +import ( + "bytes" + "testing" +) + +func TestNormalizeMySQLValueForWrite_ConvertsBitTextToBytes(t *testing.T) { + t.Parallel() + + columnTypes := map[string]string{"enabled": "bit(1)"} + + cases := []struct { + name string + value interface{} + want []byte + }{ + {name: "string one", value: "1", want: []byte{1}}, + {name: "string zero", value: "0", want: []byte{0}}, + {name: "bool true", value: true, want: []byte{1}}, + {name: "bool false", value: false, want: []byte{0}}, + {name: "float integral", value: float64(1), want: []byte{1}}, + {name: "binary literal", value: "b'1'", want: []byte{1}}, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := normalizeMySQLValueForWrite("enabled", tc.value, columnTypes) + gotBytes, ok := got.([]byte) + if !ok { + t.Fatalf("期望 bit 写入值被转换为 []byte,实际=%T(%v)", got, got) + } + if !bytes.Equal(gotBytes, tc.want) { + t.Fatalf("bit 写入值不符合预期,want=%v got=%v", tc.want, gotBytes) + } + }) + } +} + +func TestNormalizeMySQLValueForInsert_ConvertsBitTextToBytes(t *testing.T) { + t.Parallel() + + columnTypes := map[string]string{"enabled": "bit(1)"} + + got, omit := normalizeMySQLValueForInsert("enabled", "1", columnTypes) + if omit { + t.Fatalf("bit(1) 插入值不应被省略") + } + gotBytes, ok := got.([]byte) + if !ok { + t.Fatalf("期望 bit 插入值被转换为 []byte,实际=%T(%v)", got, got) + } + if !bytes.Equal(gotBytes, []byte{1}) { + t.Fatalf("bit 插入值不符合预期,want=%v got=%v", []byte{1}, gotBytes) + } +} + +func TestNormalizeMySQLValueForWrite_KeepsNonBitTextUntouched(t *testing.T) { + t.Parallel() + + columnTypes := map[string]string{"name": "varchar(255)"} + got := normalizeMySQLValueForWrite("name", "1", columnTypes) + if text, ok := got.(string); !ok || text != "1" { + t.Fatalf("非 bit 列不应被转换,实际=%T(%v)", got, got) + } +}