mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-06 20:03:05 +08:00
🐛 fix(query): 修正查询结果同名列被覆盖问题
- 为查询结果扫描增加稳定列名归一化,重复列自动追加序号后缀 - 统一返回字段列表与行数据键名,避免同名列值被后写覆盖 - 补充 scanRows 回归测试并更新 issue backlog 记录 Fixes #348
This commit is contained in:
@@ -36,6 +36,7 @@
|
||||
| #342 | 数据同步功能不能用,mysql数据库8.4版本选了结构同步,最后没同步成功 | Fixed | Pending |
|
||||
| #343 | redis删除hash类型中的key报错 | Fixed | Pending |
|
||||
| #346 | TDEngine只显示子表不显示超级表 | Fixed | Pending |
|
||||
| #348 | [Bug] sql查询同名字段,结果集不会自动添加别名 | Fixed | Pending |
|
||||
| #351 | 为什么没有截断和清空表的功能呀? | Fixed | Pending |
|
||||
|
||||
## Notes
|
||||
@@ -130,6 +131,12 @@
|
||||
- 处理:为 TDEngine 表列表查询补充 `SHOW STABLES`,与 `SHOW TABLES` 结果统一去重合并后返回,保证普通表和超级表同时可见。
|
||||
- 验证:新增 `internal/db/tdengine_applychanges_test.go` 回归测试,覆盖 `GetTables` 返回普通表 + 超级表,并执行 `go test -tags gonavi_tdengine_driver ./internal/db -count=1`。
|
||||
|
||||
### #348
|
||||
|
||||
- 根因:查询结果扫描层直接使用数据库返回的原始列名作为 `map[string]interface{}` 键。同名列场景下,后面的值会覆盖前面的值,返回给前端的 `fields/columns` 也保留重复列名,导致结果集既无法自动补别名,也拿不到两列值。
|
||||
- 处理:为 `scanRows` 增加稳定列名归一化逻辑。首次出现保留原名,重复列自动追加 `_2`、`_3` 后缀;空列名回退为 `column_N`。返回的列列表和每行数据统一使用同一套唯一列名,避免覆盖。
|
||||
- 验证:新增 `internal/db/scan_rows_test.go` 回归测试,覆盖重复列 `id/id/name` 自动归一化为 `id/id_2/name` 且两列值均保留,并执行 `go test ./internal/db -run TestScanRowsRenamesDuplicateColumns -count=1` 与 `go test ./internal/db -count=1`。
|
||||
|
||||
### #330
|
||||
|
||||
- 根因:查询结果表格已经支持拖拽调整列宽,但 resize handle 没有提供双击自适应逻辑,导致用户只能靠手工拖拽慢慢试宽度。
|
||||
|
||||
@@ -2,6 +2,7 @@ package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
)
|
||||
@@ -11,6 +12,7 @@ func scanRows(rows *sql.Rows) ([]map[string]interface{}, []string, error) {
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
columns = ensureUniqueQueryColumnNames(columns)
|
||||
|
||||
colTypes, err := rows.ColumnTypes()
|
||||
if err != nil || len(colTypes) != len(columns) {
|
||||
@@ -47,6 +49,46 @@ func scanRows(rows *sql.Rows) ([]map[string]interface{}, []string, error) {
|
||||
return resultData, columns, nil
|
||||
}
|
||||
|
||||
func ensureUniqueQueryColumnNames(columns []string) []string {
|
||||
if len(columns) == 0 {
|
||||
return columns
|
||||
}
|
||||
|
||||
uniqueColumns := make([]string, len(columns))
|
||||
taken := make(map[string]struct{}, len(columns))
|
||||
nextSuffix := make(map[string]int, len(columns))
|
||||
|
||||
for idx, column := range columns {
|
||||
base := column
|
||||
if base == "" {
|
||||
base = fmt.Sprintf("column_%d", idx+1)
|
||||
}
|
||||
|
||||
candidate := base
|
||||
if _, exists := taken[candidate]; exists {
|
||||
suffix := nextSuffix[base]
|
||||
if suffix < 2 {
|
||||
suffix = 2
|
||||
}
|
||||
for {
|
||||
candidate = fmt.Sprintf("%s_%d", base, suffix)
|
||||
if _, exists := taken[candidate]; !exists {
|
||||
break
|
||||
}
|
||||
suffix++
|
||||
}
|
||||
nextSuffix[base] = suffix + 1
|
||||
} else {
|
||||
nextSuffix[base] = 2
|
||||
}
|
||||
|
||||
uniqueColumns[idx] = candidate
|
||||
taken[candidate] = struct{}{}
|
||||
}
|
||||
|
||||
return uniqueColumns
|
||||
}
|
||||
|
||||
// scanMultiRows 遍历 sql.Rows 中的所有结果集,将每个结果集作为 ResultSetData 返回。
|
||||
// 利用 rows.NextResultSet() 支持一次 query 返回多个结果集的场景。
|
||||
func scanMultiRows(rows *sql.Rows) ([]connection.ResultSetData, error) {
|
||||
|
||||
97
internal/db/scan_rows_test.go
Normal file
97
internal/db/scan_rows_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"io"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const scanRowsDuplicateDriverName = "gonavi-scan-rows-duplicate"
|
||||
|
||||
var registerScanRowsDuplicateDriverOnce sync.Once
|
||||
|
||||
type scanRowsDuplicateDriver struct{}
|
||||
|
||||
func (scanRowsDuplicateDriver) Open(name string) (driver.Conn, error) {
|
||||
return scanRowsDuplicateConn{}, nil
|
||||
}
|
||||
|
||||
type scanRowsDuplicateConn struct{}
|
||||
|
||||
func (scanRowsDuplicateConn) Prepare(query string) (driver.Stmt, error) { return nil, driver.ErrSkip }
|
||||
func (scanRowsDuplicateConn) Close() error { return nil }
|
||||
func (scanRowsDuplicateConn) Begin() (driver.Tx, error) { return nil, driver.ErrSkip }
|
||||
|
||||
func (scanRowsDuplicateConn) QueryContext(_ context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
|
||||
return &scanRowsDuplicateRows{
|
||||
columns: []string{"id", "id", "name"},
|
||||
rows: [][]driver.Value{
|
||||
{int64(1), int64(2), "alice"},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ driver.QueryerContext = (*scanRowsDuplicateConn)(nil)
|
||||
|
||||
type scanRowsDuplicateRows struct {
|
||||
columns []string
|
||||
rows [][]driver.Value
|
||||
index int
|
||||
}
|
||||
|
||||
func (r *scanRowsDuplicateRows) Columns() []string { return append([]string(nil), r.columns...) }
|
||||
func (r *scanRowsDuplicateRows) Close() error { return nil }
|
||||
|
||||
func (r *scanRowsDuplicateRows) Next(dest []driver.Value) error {
|
||||
if r.index >= len(r.rows) {
|
||||
return io.EOF
|
||||
}
|
||||
row := r.rows[r.index]
|
||||
for idx := range dest {
|
||||
if idx < len(row) {
|
||||
dest[idx] = row[idx]
|
||||
}
|
||||
}
|
||||
r.index++
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestScanRowsRenamesDuplicateColumns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
registerScanRowsDuplicateDriverOnce.Do(func() {
|
||||
sql.Register(scanRowsDuplicateDriverName, scanRowsDuplicateDriver{})
|
||||
})
|
||||
|
||||
dbConn, err := sql.Open(scanRowsDuplicateDriverName, "")
|
||||
if err != nil {
|
||||
t.Fatalf("open duplicate scan rows db failed: %v", err)
|
||||
}
|
||||
defer dbConn.Close()
|
||||
|
||||
rows, err := dbConn.QueryContext(context.Background(), "SELECT 1")
|
||||
if err != nil {
|
||||
t.Fatalf("query duplicate scan rows db failed: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
data, columns, err := scanRows(rows)
|
||||
if err != nil {
|
||||
t.Fatalf("scanRows returned error: %v", err)
|
||||
}
|
||||
|
||||
wantColumns := []string{"id", "id_2", "name"}
|
||||
if !reflect.DeepEqual(columns, wantColumns) {
|
||||
t.Fatalf("unexpected columns: got=%v want=%v", columns, wantColumns)
|
||||
}
|
||||
if len(data) != 1 {
|
||||
t.Fatalf("expected one row, got=%d", len(data))
|
||||
}
|
||||
if data[0]["id"] != int64(1) || data[0]["id_2"] != int64(2) || data[0]["name"] != "alice" {
|
||||
t.Fatalf("unexpected row data: %#v", data[0])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user