mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-15 02:49:49 +08:00
🐛 fix(duckdb): 修复 DuckDB 查询误用连接超时导致中断
- 新增 DuckDB 查询上下文策略,避免将连接超时直接作为查询执行超时 - 调整 DBQueryWithCancel、DBQueryMulti、DBQueryIsolated 统一走查询上下文工厂 - 补充 DuckDB 查询不继承连接超时与网络型数据库保留 deadline 的回归测试
This commit is contained in:
@@ -1 +1 @@
|
||||
0295a42fd931778d85157816d79d29e5
|
||||
d0464f9da25e9356e61652e638c99ffe
|
||||
@@ -23,6 +23,17 @@ func normalizeTestConnectionConfig(config connection.ConnectionConfig) connectio
|
||||
return normalized
|
||||
}
|
||||
|
||||
func newQueryExecutionContext(config connection.ConnectionConfig) (context.Context, context.CancelFunc) {
|
||||
if strings.EqualFold(strings.TrimSpace(config.Type), "duckdb") {
|
||||
return context.WithCancel(context.Background())
|
||||
}
|
||||
timeoutSeconds := config.Timeout
|
||||
if timeoutSeconds <= 0 {
|
||||
timeoutSeconds = 30
|
||||
}
|
||||
return utils.ContextWithTimeout(time.Duration(timeoutSeconds) * time.Second)
|
||||
}
|
||||
|
||||
func validateTestConnectionInput(config connection.ConnectionConfig) error {
|
||||
dbType := strings.ToLower(strings.TrimSpace(config.Type))
|
||||
if dbType == "" {
|
||||
@@ -600,11 +611,7 @@ func (a *App) DBQueryWithCancel(config connection.ConnectionConfig, dbName strin
|
||||
}
|
||||
|
||||
query = sanitizeSQLForPgLike(resolveDDLDBType(config), query)
|
||||
timeoutSeconds := runConfig.Timeout
|
||||
if timeoutSeconds <= 0 {
|
||||
timeoutSeconds = 30
|
||||
}
|
||||
ctx, cancel := utils.ContextWithTimeout(time.Duration(timeoutSeconds) * time.Second)
|
||||
ctx, cancel := newQueryExecutionContext(runConfig)
|
||||
defer cancel()
|
||||
|
||||
// Store cancel function for potential manual cancellation
|
||||
@@ -707,11 +714,7 @@ func (a *App) DBQueryMulti(config connection.ConnectionConfig, dbName string, qu
|
||||
}
|
||||
|
||||
query = sanitizeSQLForPgLike(resolveDDLDBType(config), query)
|
||||
timeoutSeconds := runConfig.Timeout
|
||||
if timeoutSeconds <= 0 {
|
||||
timeoutSeconds = 30
|
||||
}
|
||||
ctx, cancel := utils.ContextWithTimeout(time.Duration(timeoutSeconds) * time.Second)
|
||||
ctx, cancel := newQueryExecutionContext(runConfig)
|
||||
defer cancel()
|
||||
|
||||
a.queryMu.Lock()
|
||||
@@ -1033,11 +1036,7 @@ func (a *App) DBQueryIsolated(config connection.ConnectionConfig, dbName string,
|
||||
}()
|
||||
|
||||
query = sanitizeSQLForPgLike(resolveDDLDBType(config), query)
|
||||
timeoutSeconds := runConfig.Timeout
|
||||
if timeoutSeconds <= 0 {
|
||||
timeoutSeconds = 30
|
||||
}
|
||||
ctx, cancel := utils.ContextWithTimeout(time.Duration(timeoutSeconds) * time.Second)
|
||||
ctx, cancel := newQueryExecutionContext(runConfig)
|
||||
defer cancel()
|
||||
|
||||
isReadQuery := isReadOnlySQLQuery(runConfig.Type, query)
|
||||
|
||||
@@ -147,3 +147,26 @@ func TestDBQueryWithCancel_QueryIDPropagation(t *testing.T) {
|
||||
t.Fatalf("Expected QueryID 'test-query-id' in result, got: %s", result.QueryID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewQueryExecutionContext_UsesTimeoutForNetworkDatabases(t *testing.T) {
|
||||
ctx, cancel := newQueryExecutionContext(connection.ConnectionConfig{Type: "mysql", Timeout: 7})
|
||||
defer cancel()
|
||||
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
t.Fatal("expected network database query context to carry a deadline")
|
||||
}
|
||||
remaining := time.Until(deadline)
|
||||
if remaining <= 0 || remaining > 8*time.Second {
|
||||
t.Fatalf("expected deadline around 7s, got remaining=%s", remaining)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewQueryExecutionContext_DoesNotApplyConnectTimeoutToDuckDBQueries(t *testing.T) {
|
||||
ctx, cancel := newQueryExecutionContext(connection.ConnectionConfig{Type: "duckdb", Timeout: 1})
|
||||
defer cancel()
|
||||
|
||||
if _, ok := ctx.Deadline(); ok {
|
||||
t.Fatal("expected DuckDB query context to avoid connection-timeout deadline")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ type fakeBatchWriteDB struct {
|
||||
execCalls int
|
||||
execQueries []string
|
||||
lastQuery string
|
||||
lastCtx context.Context
|
||||
queryCalls int
|
||||
queryMap map[string][]map[string]interface{}
|
||||
fieldMap map[string][]string
|
||||
@@ -87,12 +88,14 @@ func (f *fakeBatchWriteDB) GetTriggers(dbName, tableName string) ([]connection.T
|
||||
}
|
||||
|
||||
func (f *fakeBatchWriteDB) ExecContext(ctx context.Context, query string) (int64, error) {
|
||||
f.lastCtx = ctx
|
||||
f.execCalls++
|
||||
f.execQueries = append(f.execQueries, query)
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func (f *fakeBatchWriteDB) QueryContext(ctx context.Context, query string) ([]map[string]interface{}, []string, error) {
|
||||
f.lastCtx = ctx
|
||||
f.queryCalls++
|
||||
if err := f.queryErr[query]; err != nil {
|
||||
return nil, nil, err
|
||||
@@ -395,6 +398,48 @@ func TestDBQueryWithCancelReturnsMessagesForSQLServerQuery(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDBQueryWithCancel_DuckDBQueriesDoNotInheritConnectTimeout(t *testing.T) {
|
||||
originalNewDatabaseFunc := newDatabaseFunc
|
||||
originalVerifyDriverAgentRevisionFunc := verifyDriverAgentRevisionFunc
|
||||
t.Cleanup(func() {
|
||||
newDatabaseFunc = originalNewDatabaseFunc
|
||||
verifyDriverAgentRevisionFunc = originalVerifyDriverAgentRevisionFunc
|
||||
})
|
||||
|
||||
query := "SELECT 1"
|
||||
fakeDB := &fakeBatchWriteDB{
|
||||
queryMap: map[string][]map[string]interface{}{
|
||||
query: {
|
||||
{"value": 1},
|
||||
},
|
||||
},
|
||||
fieldMap: map[string][]string{
|
||||
query: {"value"},
|
||||
},
|
||||
queryErr: map[string]error{},
|
||||
}
|
||||
newDatabaseFunc = func(dbType string) (db.Database, error) {
|
||||
return fakeDB, nil
|
||||
}
|
||||
verifyDriverAgentRevisionFunc = func(config connection.ConnectionConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
app := NewAppWithSecretStore(secretstore.NewUnavailableStore("test"))
|
||||
config := connection.ConnectionConfig{Type: "duckdb", Host: ":memory:", Timeout: 1}
|
||||
|
||||
result := app.DBQueryWithCancel(config, "main", query, "duckdb-no-deadline-test")
|
||||
if !result.Success {
|
||||
t.Fatalf("expected DuckDB DBQueryWithCancel success, got failure: %s", result.Message)
|
||||
}
|
||||
if fakeDB.lastCtx == nil {
|
||||
t.Fatal("expected DuckDB query path to receive a context")
|
||||
}
|
||||
if _, ok := fakeDB.lastCtx.Deadline(); ok {
|
||||
t.Fatal("expected DuckDB query context to avoid connection-timeout deadline")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDBQueryMultiUsesBatchWriteExecerForAllWriteStatements(t *testing.T) {
|
||||
originalNewDatabaseFunc := newDatabaseFunc
|
||||
t.Cleanup(func() {
|
||||
|
||||
Reference in New Issue
Block a user