mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-11 07:21:37 +08:00
🐛 fix(query-export): 修复查询结果导出卡住并统一按数据源能力控制导出路径
- 查询结果页导出增加稳定兜底,异常时确保 loading 关闭避免持续转圈 - DataGrid 导出逻辑按数据源能力分流,优先走后端 ExportQuery 并保留结果集导出降级 - QueryEditor 传递结果导出 SQL,保证查询结果导出范围与当前结果一致 - 后端补充 ExportData/ExportQuery 关键日志,提升导出链路可观测性
This commit is contained in:
@@ -225,7 +225,7 @@ const builtinDriverManifestJSON = `{
|
||||
"vastbase": { "engine": "go", "version": "1.11.1", "checksumPolicy": "off", "downloadUrl": "builtin://activate/vastbase" },
|
||||
"mongodb": { "engine": "go", "version": "2.5.0", "checksumPolicy": "off", "downloadUrl": "builtin://activate/mongodb" },
|
||||
"tdengine": { "engine": "go", "version": "3.7.8", "checksumPolicy": "off", "downloadUrl": "builtin://activate/tdengine" },
|
||||
"clickhouse": { "engine": "go", "version": "2.43.0", "checksumPolicy": "off", "downloadUrl": "builtin://activate/clickhouse" }
|
||||
"clickhouse": { "engine": "go", "version": "2.43.1", "checksumPolicy": "off", "downloadUrl": "builtin://activate/clickhouse" }
|
||||
}
|
||||
}`
|
||||
|
||||
@@ -278,7 +278,7 @@ var latestDriverVersionMap = map[string]string{
|
||||
"vastbase": "1.11.2",
|
||||
"mongodb": "2.5.0",
|
||||
"tdengine": "3.7.8",
|
||||
"clickhouse": "2.43.0",
|
||||
"clickhouse": "2.43.1",
|
||||
"oracle": "2.9.0",
|
||||
"postgres": "1.11.2",
|
||||
"redis": "9.17.3",
|
||||
|
||||
@@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -16,11 +17,16 @@ import (
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
"GoNavi-Wails/internal/db"
|
||||
"GoNavi-Wails/internal/logger"
|
||||
"GoNavi-Wails/internal/utils"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
const minExportQueryTimeout = 5 * time.Minute
|
||||
const minClickHouseExportQueryTimeout = 2 * time.Hour
|
||||
|
||||
func (a *App) OpenSQLFile() connection.QueryResult {
|
||||
selection, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
|
||||
Title: "Select SQL File",
|
||||
@@ -614,7 +620,7 @@ func (a *App) ExportTable(config connection.ConnectionConfig, dbName string, tab
|
||||
|
||||
query := fmt.Sprintf("SELECT * FROM %s", quoteQualifiedIdentByType(runConfig.Type, tableName))
|
||||
|
||||
data, columns, err := dbInst.Query(query)
|
||||
data, columns, err := queryDataForExport(dbInst, runConfig, query)
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
@@ -945,7 +951,7 @@ func listViewNameLookup(dbInst db.Database, config connection.ConnectionConfig,
|
||||
if strings.TrimSpace(query) == "" {
|
||||
continue
|
||||
}
|
||||
rows, _, err := dbInst.Query(query)
|
||||
rows, _, err := queryDataForExport(dbInst, config, query)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -1056,7 +1062,7 @@ func tryGetViewCreateStatement(
|
||||
if strings.TrimSpace(query) == "" {
|
||||
continue
|
||||
}
|
||||
rows, _, err := dbInst.Query(query)
|
||||
rows, _, err := queryDataForExport(dbInst, config, query)
|
||||
if err != nil || len(rows) == 0 {
|
||||
continue
|
||||
}
|
||||
@@ -1421,7 +1427,7 @@ func dumpTableSQL(
|
||||
|
||||
qualified := qualifyTable(schemaName, pureTableName)
|
||||
selectSQL := fmt.Sprintf("SELECT * FROM %s", quoteQualifiedIdentByType(config.Type, qualified))
|
||||
data, columns, err := dbInst.Query(selectSQL)
|
||||
data, columns, err := queryDataForExport(dbInst, config, selectSQL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1456,14 +1462,17 @@ func (a *App) ExportData(data []map[string]interface{}, columns []string, defaul
|
||||
if defaultName == "" {
|
||||
defaultName = "export"
|
||||
}
|
||||
logger.Infof("ExportData 开始:rows=%d cols=%d format=%s defaultName=%s", len(data), len(columns), strings.ToLower(strings.TrimSpace(format)), strings.TrimSpace(defaultName))
|
||||
filename, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
Title: "Export Data",
|
||||
DefaultFilename: fmt.Sprintf("%s.%s", defaultName, strings.ToLower(format)),
|
||||
})
|
||||
|
||||
if err != nil || filename == "" {
|
||||
logger.Infof("ExportData 已取消或未选择文件:err=%v", err)
|
||||
return connection.QueryResult{Success: false, Message: "Cancelled"}
|
||||
}
|
||||
logger.Infof("ExportData 选定文件:%s", filename)
|
||||
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
@@ -1471,9 +1480,11 @@ func (a *App) ExportData(data []map[string]interface{}, columns []string, defaul
|
||||
}
|
||||
defer f.Close()
|
||||
if err := writeRowsToFile(f, data, columns, format); err != nil {
|
||||
logger.Warnf("ExportData 写入失败:file=%s err=%v", filename, err)
|
||||
return connection.QueryResult{Success: false, Message: "Write error: " + err.Error()}
|
||||
}
|
||||
|
||||
logger.Infof("ExportData 完成:file=%s rows=%d", filename, len(data))
|
||||
return connection.QueryResult{Success: true, Message: "Export successful"}
|
||||
}
|
||||
|
||||
@@ -1494,8 +1505,10 @@ func (a *App) ExportQuery(config connection.ConnectionConfig, dbName string, que
|
||||
DefaultFilename: fmt.Sprintf("%s.%s", defaultName, strings.ToLower(format)),
|
||||
})
|
||||
if err != nil || filename == "" {
|
||||
logger.Infof("ExportQuery 已取消或未选择文件:err=%v", err)
|
||||
return connection.QueryResult{Success: false, Message: "Cancelled"}
|
||||
}
|
||||
logger.Infof("ExportQuery 开始:type=%s db=%s format=%s file=%s sql=%q", strings.TrimSpace(config.Type), strings.TrimSpace(dbName), strings.ToLower(strings.TrimSpace(format)), filename, sqlSnippet(query))
|
||||
|
||||
runConfig := normalizeRunConfig(config, dbName)
|
||||
dbInst, err := a.getDatabase(runConfig)
|
||||
@@ -1509,8 +1522,9 @@ func (a *App) ExportQuery(config connection.ConnectionConfig, dbName string, que
|
||||
return connection.QueryResult{Success: false, Message: "Only SELECT/WITH queries are supported"}
|
||||
}
|
||||
|
||||
data, columns, err := dbInst.Query(query)
|
||||
data, columns, err := queryDataForExport(dbInst, runConfig, query)
|
||||
if err != nil {
|
||||
logger.Warnf("ExportQuery 查询失败:type=%s db=%s err=%v sql=%q", strings.TrimSpace(config.Type), strings.TrimSpace(dbName), err, sqlSnippet(query))
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
@@ -1521,12 +1535,55 @@ func (a *App) ExportQuery(config connection.ConnectionConfig, dbName string, que
|
||||
defer f.Close()
|
||||
|
||||
if err := writeRowsToFile(f, data, columns, format); err != nil {
|
||||
logger.Warnf("ExportQuery 写入失败:file=%s err=%v", filename, err)
|
||||
return connection.QueryResult{Success: false, Message: "Write error: " + err.Error()}
|
||||
}
|
||||
|
||||
logger.Infof("ExportQuery 完成:file=%s rows=%d cols=%d", filename, len(data), len(columns))
|
||||
return connection.QueryResult{Success: true, Message: "Export successful"}
|
||||
}
|
||||
|
||||
func queryDataForExport(dbInst db.Database, config connection.ConnectionConfig, query string) ([]map[string]interface{}, []string, error) {
|
||||
timeout := getExportQueryTimeout(config)
|
||||
dbType := resolveDDLDBType(config)
|
||||
if dbType == "clickhouse" {
|
||||
logger.Infof("ClickHouse 导出查询开始:timeout=%s SQL片段=%q", timeout, sqlSnippet(query))
|
||||
}
|
||||
if q, ok := dbInst.(interface {
|
||||
QueryContext(context.Context, string) ([]map[string]interface{}, []string, error)
|
||||
}); ok {
|
||||
ctx, cancel := utils.ContextWithTimeout(timeout)
|
||||
defer cancel()
|
||||
data, columns, err := q.QueryContext(ctx, query)
|
||||
if err != nil && dbType == "clickhouse" {
|
||||
logger.Warnf("ClickHouse 导出查询失败:timeout=%s SQL片段=%q err=%v", timeout, sqlSnippet(query), err)
|
||||
}
|
||||
return data, columns, err
|
||||
}
|
||||
data, columns, err := dbInst.Query(query)
|
||||
if err != nil && dbType == "clickhouse" {
|
||||
logger.Warnf("ClickHouse 导出查询失败(无 QueryContext):timeout=%s SQL片段=%q err=%v", timeout, sqlSnippet(query), err)
|
||||
}
|
||||
return data, columns, err
|
||||
}
|
||||
|
||||
func getExportQueryTimeout(config connection.ConnectionConfig) time.Duration {
|
||||
timeout := time.Duration(config.Timeout) * time.Second
|
||||
if timeout <= 0 {
|
||||
timeout = minExportQueryTimeout
|
||||
}
|
||||
if resolveDDLDBType(config) == "clickhouse" {
|
||||
if timeout < minClickHouseExportQueryTimeout {
|
||||
timeout = minClickHouseExportQueryTimeout
|
||||
}
|
||||
return timeout
|
||||
}
|
||||
if timeout < minExportQueryTimeout {
|
||||
timeout = minExportQueryTimeout
|
||||
}
|
||||
return timeout
|
||||
}
|
||||
|
||||
func writeRowsToFile(f *os.File, data []map[string]interface{}, columns []string, format string) error {
|
||||
format = strings.ToLower(strings.TrimSpace(format))
|
||||
if f == nil {
|
||||
|
||||
@@ -2,12 +2,65 @@ package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
)
|
||||
|
||||
type fakeExportQueryDB struct {
|
||||
data []map[string]interface{}
|
||||
cols []string
|
||||
err error
|
||||
|
||||
lastQuery string
|
||||
lastContextTimeout time.Duration
|
||||
hasContextDeadline bool
|
||||
}
|
||||
|
||||
func (f *fakeExportQueryDB) Connect(config connection.ConnectionConfig) error { return nil }
|
||||
func (f *fakeExportQueryDB) Close() error { return nil }
|
||||
func (f *fakeExportQueryDB) Ping() error { return nil }
|
||||
func (f *fakeExportQueryDB) Query(query string) ([]map[string]interface{}, []string, error) {
|
||||
f.lastQuery = query
|
||||
return f.data, f.cols, f.err
|
||||
}
|
||||
func (f *fakeExportQueryDB) QueryContext(ctx context.Context, query string) ([]map[string]interface{}, []string, error) {
|
||||
f.lastQuery = query
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
f.hasContextDeadline = true
|
||||
f.lastContextTimeout = time.Until(deadline)
|
||||
}
|
||||
return f.data, f.cols, f.err
|
||||
}
|
||||
func (f *fakeExportQueryDB) Exec(query string) (int64, error) { return 0, nil }
|
||||
func (f *fakeExportQueryDB) GetDatabases() ([]string, error) { return nil, nil }
|
||||
func (f *fakeExportQueryDB) GetTables(dbName string) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakeExportQueryDB) GetCreateStatement(dbName, tableName string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
func (f *fakeExportQueryDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefinition, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakeExportQueryDB) GetAllColumns(dbName string) ([]connection.ColumnDefinitionWithTable, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakeExportQueryDB) GetIndexes(dbName, tableName string) ([]connection.IndexDefinition, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakeExportQueryDB) GetForeignKeys(dbName, tableName string) ([]connection.ForeignKeyDefinition, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakeExportQueryDB) GetTriggers(dbName, tableName string) ([]connection.TriggerDefinition, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestFormatExportCellText_FloatNoScientificNotation(t *testing.T) {
|
||||
got := formatExportCellText(1.445663e+06)
|
||||
if strings.Contains(strings.ToLower(got), "e+") || strings.Contains(strings.ToLower(got), "e-") {
|
||||
@@ -87,3 +140,66 @@ func TestWriteRowsToFile_JSON_NumberKeepPlainText(t *testing.T) {
|
||||
t.Fatalf("json 数值格式异常,want=1445663 got=%s", decoded[0]["id"].String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryDataForExport_UsesMinimumTimeout(t *testing.T) {
|
||||
fake := &fakeExportQueryDB{
|
||||
data: []map[string]interface{}{{"v": 1}},
|
||||
cols: []string{"v"},
|
||||
}
|
||||
_, _, err := queryDataForExport(fake, connection.ConnectionConfig{Timeout: 10}, "SELECT 1")
|
||||
if err != nil {
|
||||
t.Fatalf("queryDataForExport 返回错误: %v", err)
|
||||
}
|
||||
if !fake.hasContextDeadline {
|
||||
t.Fatal("queryDataForExport 应设置 context deadline")
|
||||
}
|
||||
if fake.lastQuery != "SELECT 1" {
|
||||
t.Fatalf("queryDataForExport 查询语句异常,want=%q got=%q", "SELECT 1", fake.lastQuery)
|
||||
}
|
||||
lowerBound := minExportQueryTimeout - 5*time.Second
|
||||
upperBound := minExportQueryTimeout + 5*time.Second
|
||||
if fake.lastContextTimeout < lowerBound || fake.lastContextTimeout > upperBound {
|
||||
t.Fatalf("导出最小超时异常,want≈%s got=%s", minExportQueryTimeout, fake.lastContextTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryDataForExport_UsesLargerConfiguredTimeout(t *testing.T) {
|
||||
fake := &fakeExportQueryDB{
|
||||
data: []map[string]interface{}{{"v": 1}},
|
||||
cols: []string{"v"},
|
||||
}
|
||||
_, _, err := queryDataForExport(fake, connection.ConnectionConfig{Timeout: 900}, "SELECT 1")
|
||||
if err != nil {
|
||||
t.Fatalf("queryDataForExport 返回错误: %v", err)
|
||||
}
|
||||
if !fake.hasContextDeadline {
|
||||
t.Fatal("queryDataForExport 应设置 context deadline")
|
||||
}
|
||||
expected := 900 * time.Second
|
||||
lowerBound := expected - 5*time.Second
|
||||
upperBound := expected + 5*time.Second
|
||||
if fake.lastContextTimeout < lowerBound || fake.lastContextTimeout > upperBound {
|
||||
t.Fatalf("导出配置超时异常,want≈%s got=%s", expected, fake.lastContextTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetExportQueryTimeout_ClickHouseUsesLongerMinimum(t *testing.T) {
|
||||
timeout := getExportQueryTimeout(connection.ConnectionConfig{
|
||||
Type: "clickhouse",
|
||||
Timeout: 30,
|
||||
})
|
||||
if timeout != minClickHouseExportQueryTimeout {
|
||||
t.Fatalf("clickhouse 导出超时下限异常,want=%s got=%s", minClickHouseExportQueryTimeout, timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetExportQueryTimeout_CustomClickHouseUsesLongerMinimum(t *testing.T) {
|
||||
timeout := getExportQueryTimeout(connection.ConnectionConfig{
|
||||
Type: "custom",
|
||||
Driver: "clickhouse",
|
||||
Timeout: 30,
|
||||
})
|
||||
if timeout != minClickHouseExportQueryTimeout {
|
||||
t.Fatalf("custom clickhouse 导出超时下限异常,want=%s got=%s", minClickHouseExportQueryTimeout, timeout)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ const (
|
||||
defaultClickHousePort = 9000
|
||||
defaultClickHouseUser = "default"
|
||||
defaultClickHouseDatabase = "default"
|
||||
minClickHouseReadTimeout = 5 * time.Minute
|
||||
)
|
||||
|
||||
type ClickHouseDB struct {
|
||||
@@ -101,7 +102,11 @@ func applyClickHouseURI(config connection.ConnectionConfig) connection.Connectio
|
||||
}
|
||||
|
||||
func (c *ClickHouseDB) buildClickHouseOptions(config connection.ConnectionConfig) *clickhouse.Options {
|
||||
timeout := getConnectTimeout(config)
|
||||
connectTimeout := getConnectTimeout(config)
|
||||
readTimeout := connectTimeout
|
||||
if readTimeout < minClickHouseReadTimeout {
|
||||
readTimeout = minClickHouseReadTimeout
|
||||
}
|
||||
return &clickhouse.Options{
|
||||
Addr: []string{
|
||||
net.JoinHostPort(config.Host, strconv.Itoa(config.Port)),
|
||||
@@ -111,8 +116,8 @@ func (c *ClickHouseDB) buildClickHouseOptions(config connection.ConnectionConfig
|
||||
Username: strings.TrimSpace(config.User),
|
||||
Password: config.Password,
|
||||
},
|
||||
DialTimeout: timeout,
|
||||
ReadTimeout: timeout,
|
||||
DialTimeout: connectTimeout,
|
||||
ReadTimeout: readTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ func TestClickHouseOptions_UsesStructuredTimeoutAndAuth(t *testing.T) {
|
||||
if opts.DialTimeout != 15*time.Second {
|
||||
t.Fatalf("dial timeout 不符合预期:%s", opts.DialTimeout)
|
||||
}
|
||||
if opts.ReadTimeout != 15*time.Second {
|
||||
if opts.ReadTimeout != minClickHouseReadTimeout {
|
||||
t.Fatalf("read timeout 不符合预期:%s", opts.ReadTimeout)
|
||||
}
|
||||
if _, ok := opts.Settings["write_timeout"]; ok {
|
||||
@@ -160,3 +160,27 @@ func TestClickHouseOptions_UsesStructuredTimeoutAndAuth(t *testing.T) {
|
||||
t.Fatalf("options 不应通过 settings 传递 dial_timeout:%v", opts.Settings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClickHouseOptions_ReadTimeoutUsesLargerConfiguredTimeout(t *testing.T) {
|
||||
c := &ClickHouseDB{}
|
||||
cfg := normalizeClickHouseConfig(connection.ConnectionConfig{
|
||||
Type: "clickhouse",
|
||||
Host: "127.0.0.1",
|
||||
Port: 9000,
|
||||
User: "default",
|
||||
Password: "secret",
|
||||
Database: "analytics",
|
||||
Timeout: 900,
|
||||
})
|
||||
|
||||
opts := c.buildClickHouseOptions(cfg)
|
||||
if opts == nil {
|
||||
t.Fatal("options 为空")
|
||||
}
|
||||
if opts.DialTimeout != 900*time.Second {
|
||||
t.Fatalf("dial timeout 不符合预期:%s", opts.DialTimeout)
|
||||
}
|
||||
if opts.ReadTimeout != 900*time.Second {
|
||||
t.Fatalf("read timeout 不符合预期:%s", opts.ReadTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,10 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
"GoNavi-Wails/internal/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -38,6 +40,7 @@ type optionalAgentRequest struct {
|
||||
Method string `json:"method"`
|
||||
Config *connection.ConnectionConfig `json:"config,omitempty"`
|
||||
Query string `json:"query,omitempty"`
|
||||
TimeoutMs int64 `json:"timeoutMs,omitempty"`
|
||||
DBName string `json:"dbName,omitempty"`
|
||||
TableName string `json:"tableName,omitempty"`
|
||||
Changes *connection.ChangeSet `json:"changes,omitempty"`
|
||||
@@ -223,6 +226,7 @@ func (d *OptionalDriverAgentDB) Connect(config connection.ConnectionConfig) erro
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("%s 驱动代理路径:%s", driverDisplayName(d.driverType), executablePath)
|
||||
client, err := newOptionalDriverAgentClient(d.driverType, executablePath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -260,7 +264,20 @@ func (d *OptionalDriverAgentDB) QueryContext(ctx context.Context, query string)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return d.Query(query)
|
||||
client, err := d.requireClient()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var data []map[string]interface{}
|
||||
var fields []string
|
||||
if err := client.call(optionalAgentRequest{
|
||||
Method: optionalAgentMethodQuery,
|
||||
Query: query,
|
||||
TimeoutMs: timeoutMsFromContext(ctx),
|
||||
}, &data, &fields, nil); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return data, fields, nil
|
||||
}
|
||||
|
||||
func (d *OptionalDriverAgentDB) Query(query string) ([]map[string]interface{}, []string, error) {
|
||||
@@ -283,7 +300,19 @@ func (d *OptionalDriverAgentDB) ExecContext(ctx context.Context, query string) (
|
||||
if err := ctx.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return d.Exec(query)
|
||||
client, err := d.requireClient()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var affected int64
|
||||
if err := client.call(optionalAgentRequest{
|
||||
Method: optionalAgentMethodExec,
|
||||
Query: query,
|
||||
TimeoutMs: timeoutMsFromContext(ctx),
|
||||
}, nil, nil, &affected); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return affected, nil
|
||||
}
|
||||
|
||||
func (d *OptionalDriverAgentDB) Exec(query string) (int64, error) {
|
||||
@@ -443,3 +472,15 @@ func (d *OptionalDriverAgentDB) requireClient() (*optionalDriverAgentClient, err
|
||||
}
|
||||
return d.client, nil
|
||||
}
|
||||
|
||||
func timeoutMsFromContext(ctx context.Context) int64 {
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
remaining := time.Until(deadline).Milliseconds()
|
||||
if remaining <= 0 {
|
||||
return 1
|
||||
}
|
||||
return remaining
|
||||
}
|
||||
|
||||
32
internal/db/optional_driver_agent_impl_test.go
Normal file
32
internal/db/optional_driver_agent_impl_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTimeoutMsFromContext_NoDeadline(t *testing.T) {
|
||||
if got := timeoutMsFromContext(context.Background()); got != 0 {
|
||||
t.Fatalf("无 deadline 时应返回 0,got=%d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeoutMsFromContext_WithDeadline(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
got := timeoutMsFromContext(ctx)
|
||||
if got <= 0 {
|
||||
t.Fatalf("有 deadline 时应返回正值,got=%d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeoutMsFromContext_ExpiredDeadline(t *testing.T) {
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
|
||||
defer cancel()
|
||||
|
||||
if got := timeoutMsFromContext(ctx); got != 1 {
|
||||
t.Fatalf("过期 deadline 应返回 1,got=%d", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user