mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-07 06:13:03 +08:00
41
docs/issues/2026-04-11-issue-backlog-tracking.md
Normal file
41
docs/issues/2026-04-11-issue-backlog-tracking.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 2026-04-11 Issue Backlog Tracking
|
||||
|
||||
## Scope
|
||||
|
||||
- 分支:`codex/issue-242-data-root`
|
||||
- 策略:按 GitHub issue 创建时间从早到晚逐条处理
|
||||
- 提交要求:每条 issue 单独本地提交,提交信息使用 `Fixes #<issue>`
|
||||
|
||||
## 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。
|
||||
@@ -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) {
|
||||
|
||||
68
internal/db/mysql_value_test.go
Normal file
68
internal/db/mysql_value_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user