From a718c41d5d4f87a2db29782277f615c5efb30520 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Thu, 4 Jun 2026 22:25:08 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20test(app):=20=E8=A1=A5=E5=BC=BA=20D?= =?UTF-8?q?uckDB=20=E5=AE=9A=E4=B9=89=E5=88=B7=E6=96=B0=E4=B8=8E=E4=B8=BB?= =?UTF-8?q?=E9=94=AE=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 补充 DuckDB 对象修改链路的真实 DDL 刷新回归测试 - 为 app 层连接门禁增加可替换测试 seam,避免 fake metadata 测试被驱动校验拦截 - 修正 DuckDB metadata 测试的串行与断言稳定性 --- internal/app/app.go | 10 +- .../app/methods_db_metadata_retry_test.go | 168 +++++++++++++++++- 2 files changed, 173 insertions(+), 5 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 51b9bf6..d394f7d 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -36,6 +36,8 @@ const ( var ( newDatabaseFunc = db.NewDatabase resolveDialConfigWithProxyFunc = resolveDialConfigWithProxy + driverRuntimeSupportStatusFunc = db.DriverRuntimeSupportStatus + verifyDriverAgentRevisionFunc = verifyRuntimeOptionalDriverAgentRevision ) type cachedDatabase struct { @@ -556,13 +558,13 @@ func (a *App) openDatabaseIsolated(config connection.ConnectionConfig) (db.Datab return nil, wrapConnectError(config, err) } effectiveConfig := applyGlobalProxyToConnection(resolvedConfig) - if supported, reason := db.DriverRuntimeSupportStatus(effectiveConfig.Type); !supported { + if supported, reason := driverRuntimeSupportStatusFunc(effectiveConfig.Type); !supported { if strings.TrimSpace(reason) == "" { reason = fmt.Sprintf("%s 驱动未启用,请先在驱动管理中安装启用", strings.TrimSpace(effectiveConfig.Type)) } return nil, withLogHint{err: fmt.Errorf("%s", reason), logPath: logger.Path()} } - if revisionErr := verifyRuntimeOptionalDriverAgentRevision(effectiveConfig); revisionErr != nil { + if revisionErr := verifyDriverAgentRevisionFunc(effectiveConfig); revisionErr != nil { return nil, withLogHint{err: revisionErr, logPath: logger.Path()} } @@ -600,7 +602,7 @@ func (a *App) getDatabaseWithPing(config connection.ConnectionConfig, forcePing strings.TrimSpace(effectiveConfig.Type), rawDSN, normalizedDSN, effectiveConfig.Timeout, forcePing, shortKey) } - if supported, reason := db.DriverRuntimeSupportStatus(effectiveConfig.Type); !supported { + if supported, reason := driverRuntimeSupportStatusFunc(effectiveConfig.Type); !supported { if strings.TrimSpace(reason) == "" { reason = fmt.Sprintf("%s 驱动未启用,请先在驱动管理中安装启用", strings.TrimSpace(effectiveConfig.Type)) } @@ -677,7 +679,7 @@ func (a *App) getDatabaseWithPing(config connection.ConnectionConfig, forcePing formatConnSummary(effectiveConfig), shortKey, formatConnectFailureCooldown(remaining), normalizeErrorMessage(failure.err)) return nil, withLogHint{err: fmt.Errorf("%s", message), logPath: logger.Path()} } - if revisionErr := verifyRuntimeOptionalDriverAgentRevision(effectiveConfig); revisionErr != nil { + if revisionErr := verifyDriverAgentRevisionFunc(effectiveConfig); revisionErr != nil { return nil, withLogHint{err: revisionErr, logPath: logger.Path()} } diff --git a/internal/app/methods_db_metadata_retry_test.go b/internal/app/methods_db_metadata_retry_test.go index a571d18..305916d 100644 --- a/internal/app/methods_db_metadata_retry_test.go +++ b/internal/app/methods_db_metadata_retry_test.go @@ -2,7 +2,9 @@ package app import ( "errors" + "fmt" "path/filepath" + "strings" "testing" "GoNavi-Wails/internal/connection" @@ -239,9 +241,13 @@ func TestDBGetColumnsKeepsDatabaseForMySQLMetadata(t *testing.T) { func TestDBGetColumnsKeepsDuckDBQualifiedTableMetadata(t *testing.T) { originalNewDatabaseFunc := newDatabaseFunc originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc + originalDriverRuntimeSupportStatusFunc := driverRuntimeSupportStatusFunc + originalVerifyDriverAgentRevisionFunc := verifyDriverAgentRevisionFunc t.Cleanup(func() { newDatabaseFunc = originalNewDatabaseFunc resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc + driverRuntimeSupportStatusFunc = originalDriverRuntimeSupportStatusFunc + verifyDriverAgentRevisionFunc = originalVerifyDriverAgentRevisionFunc }) dbInst := &fakeMetadataRetryDB{ @@ -253,6 +259,12 @@ func TestDBGetColumnsKeepsDuckDBQualifiedTableMetadata(t *testing.T) { resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) { return raw, nil } + driverRuntimeSupportStatusFunc = func(driverType string) (bool, string) { + return true, "" + } + verifyDriverAgentRevisionFunc = func(config connection.ConnectionConfig) error { + return nil + } app := NewAppWithSecretStore(secretstore.NewUnavailableStore("test")) result := app.DBGetColumns(connection.ConnectionConfig{ @@ -324,9 +336,13 @@ func TestDBGetIndexesRetriesAfterCachedConnectionRefresh(t *testing.T) { func TestDBGetIndexesKeepsDuckDBQualifiedTableMetadata(t *testing.T) { originalNewDatabaseFunc := newDatabaseFunc originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc + originalDriverRuntimeSupportStatusFunc := driverRuntimeSupportStatusFunc + originalVerifyDriverAgentRevisionFunc := verifyDriverAgentRevisionFunc t.Cleanup(func() { newDatabaseFunc = originalNewDatabaseFunc resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc + driverRuntimeSupportStatusFunc = originalDriverRuntimeSupportStatusFunc + verifyDriverAgentRevisionFunc = originalVerifyDriverAgentRevisionFunc }) dbInst := &fakeMetadataRetryDB{ @@ -338,6 +354,12 @@ func TestDBGetIndexesKeepsDuckDBQualifiedTableMetadata(t *testing.T) { resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) { return raw, nil } + driverRuntimeSupportStatusFunc = func(driverType string) (bool, string) { + return true, "" + } + verifyDriverAgentRevisionFunc = func(config connection.ConnectionConfig) error { + return nil + } app := NewAppWithSecretStore(secretstore.NewUnavailableStore("test")) result := app.DBGetIndexes(connection.ConnectionConfig{ @@ -354,7 +376,6 @@ func TestDBGetIndexesKeepsDuckDBQualifiedTableMetadata(t *testing.T) { } func TestDuckDBMetadataEndpointsReturnPrimaryKeyForQualifiedTableName(t *testing.T) { - t.Parallel() requireDuckDBOptionalDriverRuntime(t) originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc @@ -423,3 +444,148 @@ CREATE UNIQUE INDEX idx_events_name ON main.events(name); t.Fatalf("expected DuckDB primary key index metadata, got %#v", indexes) } } + +func TestDuckDBDefinitionQueriesReloadLatestDDLForObjectEditFlow(t *testing.T) { + requireDuckDBOptionalDriverRuntime(t) + + originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc + t.Cleanup(func() { + resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc + }) + resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) { + return raw, nil + } + + dbPath := filepath.Join(t.TempDir(), "duckdb-definition-reload.duckdb") + app := NewAppWithSecretStore(secretstore.NewUnavailableStore("test")) + config := connection.ConnectionConfig{ + Type: "duckdb", + Host: dbPath, + } + t.Cleanup(func() { + app.invalidateCachedDatabase(config, nil) + }) + + createResult := app.DBQuery(config, "main", ` +CREATE VIEW main.active_users AS +SELECT id FROM (VALUES (1), (2)) AS users(id); + +CREATE OR REPLACE MACRO main.add_one(x) AS x + 1; +`) + if !createResult.Success { + t.Fatalf("expected DuckDB setup success, got failure: %s", createResult.Message) + } + + viewDefinitionBefore := app.DBQuery(config, "main", ` +SELECT view_definition +FROM information_schema.views +WHERE table_schema = 'main' AND table_name = 'active_users' +LIMIT 1`) + if !viewDefinitionBefore.Success { + t.Fatalf("expected initial view definition query success, got failure: %s", viewDefinitionBefore.Message) + } + viewRowsBefore, ok := viewDefinitionBefore.Data.([]map[string]interface{}) + if !ok || len(viewRowsBefore) != 1 { + t.Fatalf("expected one initial view definition row, got %#v", viewDefinitionBefore.Data) + } + viewTextBefore := strings.TrimSpace(stringValueIgnoreCase(viewRowsBefore[0], "view_definition")) + if !strings.Contains(viewTextBefore, "SELECT id FROM") || !strings.Contains(viewTextBefore, "VALUES (1), (2)") { + t.Fatalf("unexpected initial view definition: %q", viewTextBefore) + } + + routineDefinitionBefore := app.DBQuery(config, "main", ` +SELECT schema_name, function_name, parameters, macro_definition +FROM duckdb_functions() +WHERE internal = false + AND lower(function_type) = 'macro' + AND schema_name = 'main' + AND function_name = 'add_one' +LIMIT 1`) + if !routineDefinitionBefore.Success { + t.Fatalf("expected initial routine definition query success, got failure: %s", routineDefinitionBefore.Message) + } + routineRowsBefore, ok := routineDefinitionBefore.Data.([]map[string]interface{}) + if !ok || len(routineRowsBefore) != 1 { + t.Fatalf("expected one initial routine definition row, got %#v", routineDefinitionBefore.Data) + } + routineTextBefore := strings.TrimSpace(stringValueIgnoreCase(routineRowsBefore[0], "macro_definition")) + if !strings.Contains(routineTextBefore, "x + 1") { + t.Fatalf("unexpected initial routine definition: %q", routineTextBefore) + } + + replaceResult := app.DBQuery(config, "main", ` +CREATE OR REPLACE VIEW main.active_users AS +SELECT id, id * 10 AS score FROM (VALUES (1), (2)) AS users(id); + +CREATE OR REPLACE MACRO main.add_one(x) AS x + 2; +`) + if !replaceResult.Success { + t.Fatalf("expected DuckDB replace success, got failure: %s", replaceResult.Message) + } + + viewDefinitionAfter := app.DBQuery(config, "main", ` +SELECT view_definition +FROM information_schema.views +WHERE table_schema = 'main' AND table_name = 'active_users' +LIMIT 1`) + if !viewDefinitionAfter.Success { + t.Fatalf("expected latest view definition query success, got failure: %s", viewDefinitionAfter.Message) + } + viewRowsAfter, ok := viewDefinitionAfter.Data.([]map[string]interface{}) + if !ok || len(viewRowsAfter) != 1 { + t.Fatalf("expected one latest view definition row, got %#v", viewDefinitionAfter.Data) + } + viewTextAfter := strings.TrimSpace(stringValueIgnoreCase(viewRowsAfter[0], "view_definition")) + if !strings.Contains(viewTextAfter, "score") || !strings.Contains(viewTextAfter, "10") { + t.Fatalf("expected latest view definition, got %q", viewTextAfter) + } + if viewTextAfter == viewTextBefore { + t.Fatalf("expected latest view definition to differ from initial definition, got %q", viewTextAfter) + } + + routineDefinitionAfter := app.DBQuery(config, "main", ` +SELECT schema_name, function_name, parameters, macro_definition +FROM duckdb_functions() +WHERE internal = false + AND lower(function_type) = 'macro' + AND schema_name = 'main' + AND function_name = 'add_one' +LIMIT 1`) + if !routineDefinitionAfter.Success { + t.Fatalf("expected latest routine definition query success, got failure: %s", routineDefinitionAfter.Message) + } + routineRowsAfter, ok := routineDefinitionAfter.Data.([]map[string]interface{}) + if !ok || len(routineRowsAfter) != 1 { + t.Fatalf("expected one latest routine definition row, got %#v", routineDefinitionAfter.Data) + } + routineTextAfter := strings.TrimSpace(stringValueIgnoreCase(routineRowsAfter[0], "macro_definition")) + if !strings.Contains(routineTextAfter, "x + 2") { + t.Fatalf("expected latest routine definition, got %q", routineTextAfter) + } + if routineTextAfter == routineTextBefore { + t.Fatalf("expected latest routine definition to differ from initial definition, got %q", routineTextAfter) + } +} + +func stringValueIgnoreCase(row map[string]interface{}, key string) string { + for candidate, value := range row { + if strings.EqualFold(strings.TrimSpace(candidate), strings.TrimSpace(key)) { + return toStringValue(value) + } + } + return "" +} + +func toStringValue(value interface{}) string { + switch typed := value.(type) { + case string: + return typed + case []byte: + return string(typed) + default: + if value == nil { + return "" + } + return fmt.Sprint(value) + } +}