mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-15 02:49:49 +08:00
✅ test(app): 补强 DuckDB 定义刷新与主键回归
- 补充 DuckDB 对象修改链路的真实 DDL 刷新回归测试 - 为 app 层连接门禁增加可替换测试 seam,避免 fake metadata 测试被驱动校验拦截 - 修正 DuckDB metadata 测试的串行与断言稳定性
This commit is contained in:
@@ -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()}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user