From d57081ecfb39552e38a2d06afb6b1030f1092f8e Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 17 Apr 2026 13:24:50 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(query):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E7=BB=93=E6=9E=9C=E5=90=8C=E5=90=8D=E5=88=97?= =?UTF-8?q?=E8=A2=AB=E8=A6=86=E7=9B=96=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为查询结果扫描增加稳定列名归一化,重复列自动追加序号后缀 - 统一返回字段列表与行数据键名,避免同名列值被后写覆盖 - 补充 scanRows 回归测试并更新 issue backlog 记录 Fixes #348 --- .../2026-04-11-issue-backlog-tracking.md | 7 ++ internal/db/scan_rows.go | 42 ++++++++ internal/db/scan_rows_test.go | 97 +++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 internal/db/scan_rows_test.go diff --git a/docs/issues/2026-04-11-issue-backlog-tracking.md b/docs/issues/2026-04-11-issue-backlog-tracking.md index 1126a9b..5d1e0a3 100644 --- a/docs/issues/2026-04-11-issue-backlog-tracking.md +++ b/docs/issues/2026-04-11-issue-backlog-tracking.md @@ -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 没有提供双击自适应逻辑,导致用户只能靠手工拖拽慢慢试宽度。 diff --git a/internal/db/scan_rows.go b/internal/db/scan_rows.go index 3810e66..e1959ef 100644 --- a/internal/db/scan_rows.go +++ b/internal/db/scan_rows.go @@ -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) { diff --git a/internal/db/scan_rows_test.go b/internal/db/scan_rows_test.go new file mode 100644 index 0000000..91c5a62 --- /dev/null +++ b/internal/db/scan_rows_test.go @@ -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]) + } +}