🐛 fix(driver-manager/sql-editor): 优化驱动代理更新提示和事务提交控件

- 调整 driver-agent revision 为提示性校验,允许旧代理继续安装使用并保留需重装提示

- 精简 SQL 编辑器 DML 事务模式与自动提交档位展示

- 补充旧 revision 安装回归和事务控件断言
This commit is contained in:
Syngnat
2026-06-12 12:57:47 +08:00
parent d1aa06d537
commit 4cc8ab6482
5 changed files with 104 additions and 110 deletions

View File

@@ -3727,9 +3727,12 @@ describe('QueryEditor external SQL save', () => {
expect(transactionSettingsSource).toContain('gn-v2-query-toolbar-transaction-mode-select');
expect(transactionSettingsSource).toContain('gn-v2-query-toolbar-transaction-delay-select');
expect(transactionSettingsSource).toContain('参考 DBeaver');
expect(transactionSettingsSource).toContain("label: '手动提交'");
expect(transactionSettingsSource).toContain("label: '自动提交'");
expect(transactionSettingsSource).toContain("label: '手动'");
expect(transactionSettingsSource).toContain("label: '自动'");
expect(transactionSettingsSource).not.toContain("label: '手动提交'");
expect(transactionSettingsSource).not.toContain("label: '自动提交'");
expect(transactionSettingsSource).toContain("label: '立即'");
expect(transactionSettingsSource).toContain("label: '3s'");
expect(source).toContain('QueryEditorTransactionToolbar');
expect(transactionToolbarSource).toContain("className={isV2Ui ? 'gn-v2-query-transaction-toolbar' : undefined}");
expect(transactionToolbarSource).toContain("'未提交'");
@@ -3738,12 +3741,15 @@ describe('QueryEditor external SQL save', () => {
expect(transactionToolbarSource).toContain("'自动提交中'");
expect(transactionToolbarSource).toContain('onFinish');
expect(toolbarSource).toContain('gn-v2-query-toolbar-action-group');
expect(transactionSettingsSource).toContain('style={isV2Ui ? undefined : { width: 118 }}');
expect(transactionSettingsSource).toContain('style={isV2Ui ? undefined : { width: 78 }}');
expect(transactionSettingsSource).toContain('style={isV2Ui ? undefined : { width: 68 }}');
expect(toolbarSource).toContain('style={isV2Ui ? undefined : { width: 200 }}');
expect(toolbarSource).toContain('style={isV2Ui ? undefined : { width: 170 }}');
expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-toolbar-selects');
expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-toolbar-actions');
expect(css).toContain('width: 74px !important;');
expect(css).toContain('width: 62px !important;');
expect(css).toContain('flex: 0 1 auto !important;');
expect(css).toContain('justify-content: flex-start;');
expect(css).toContain('height: 32px !important;');
@@ -3755,8 +3761,6 @@ describe('QueryEditor external SQL save', () => {
expect(css).toContain('width: 140px !important;');
expect(css).toContain('width: 166px !important;');
expect(css).toContain('width: 132px !important;');
expect(css).toContain('width: 118px !important;');
expect(css).toContain('width: 82px !important;');
expect(css).toContain('width: 34px !important;');
expect(css).toContain('@media (max-width: 900px)');

View File

@@ -5,10 +5,10 @@ export type SqlEditorCommitMode = 'manual' | 'auto';
export const SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS = [
{ value: 0, label: '立即' },
{ value: 3000, label: '3 秒后' },
{ value: 5000, label: '5 秒后' },
{ value: 10000, label: '10 秒后' },
{ value: 30000, label: '30 秒后' },
{ value: 3000, label: '3s' },
{ value: 5000, label: '5s' },
{ value: 10000, label: '10s' },
{ value: 30000, label: '30s' },
];
type QueryEditorTransactionSettingsProps = {
@@ -30,19 +30,19 @@ const QueryEditorTransactionSettings: React.FC<QueryEditorTransactionSettingsPro
<Tooltip title="参考 DBeaverSQL 编辑器执行 INSERT/UPDATE/DELETE/MERGE/REPLACE 等 DML 时先进入 GoNavi 托管事务;手动提交需要手动提交/回滚,自动提交会在执行成功后自动 COMMIT。">
<Select
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-transaction-mode-select' : undefined}
style={isV2Ui ? undefined : { width: 118 }}
style={isV2Ui ? undefined : { width: 78 }}
value={commitMode}
onChange={(mode) => onCommitModeChange(mode === 'auto' ? 'auto' : 'manual')}
options={[
{ label: '手动提交', value: 'manual' },
{ label: '自动提交', value: 'auto' },
{ label: '手动', value: 'manual' },
{ label: '自动', value: 'auto' },
]}
/>
</Tooltip>
{commitMode === 'auto' && (
<Select
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-transaction-delay-select' : undefined}
style={isV2Ui ? undefined : { width: 96 }}
style={isV2Ui ? undefined : { width: 68 }}
value={autoCommitDelayMs}
onChange={(delayMs) => onAutoCommitDelayMsChange(Number(delayMs))}
options={SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS}

View File

@@ -4840,13 +4840,13 @@ body[data-ui-version="v2"] .gn-v2-query-toolbar-max-rows-select {
}
body[data-ui-version="v2"] .gn-v2-query-toolbar-transaction-mode-select {
width: 118px !important;
flex: 0 0 118px !important;
width: 74px !important;
flex: 0 0 74px !important;
}
body[data-ui-version="v2"] .gn-v2-query-toolbar-transaction-delay-select {
width: 82px !important;
flex: 0 0 82px !important;
width: 62px !important;
flex: 0 0 62px !important;
}
body[data-ui-version="v2"] .gn-v2-query-toolbar .ant-select-selector {
@@ -4891,15 +4891,19 @@ body[data-ui-version="v2"] .gn-v2-query-transaction-commit-button {
display: inline-flex !important;
align-items: center;
gap: 6px;
border-color: transparent !important;
background: var(--gn-accent-soft) !important;
color: var(--gn-accent-2) !important;
font-weight: 750 !important;
}
body[data-ui-version="v2"] .gn-v2-query-transaction-commit-button .gn-v2-toolbar-kbd {
margin-left: 0;
min-width: 18px;
justify-content: center;
background: rgba(255, 255, 255, 0.18);
color: #fff;
border-color: rgba(255, 255, 255, 0.22);
border: none;
background: rgba(22, 163, 74, 0.18);
color: var(--gn-accent-2);
}
body[data-ui-version="v2"] .gn-v2-query-toolbar-icon-action.ant-btn,

View File

@@ -2868,6 +2868,33 @@ func verifyInstalledOptionalDriverAgentRevision(driverType string, executablePat
return actual, nil
}
func observeInstalledOptionalDriverAgentRevision(driverType string, executablePath string, selectedVersion string) string {
if !shouldVerifyOptionalDriverAgentRevision(driverType, selectedVersion) {
return ""
}
expected := strings.TrimSpace(db.OptionalDriverAgentRevision(driverType))
actual, current, err := optionalDriverAgentRevisionCurrent(driverType, executablePath)
if expected == "" {
return strings.TrimSpace(actual)
}
displayName := resolveDriverDisplayName(driverDefinition{Type: driverType})
if err != nil {
logger.Warnf("%s 驱动代理版本元数据不可用已保留安装path=%s version=%s err=%v建议在驱动管理中重装",
displayName, executablePath, normalizeVersion(selectedVersion), err)
return ""
}
actual = strings.TrimSpace(actual)
if !current {
actualLabel := actual
if actualLabel == "" {
actualLabel = "空"
}
logger.Warnf("%s 驱动代理 revision 不匹配,已保留安装:已安装=%s 当前需要=%s path=%s version=%s建议在驱动管理中重装",
displayName, actualLabel, expected, executablePath, normalizeVersion(selectedVersion))
}
return actual
}
func shouldVerifyOptionalDriverAgentRevision(driverType string, selectedVersion string) bool {
switch normalizeDriverType(driverType) {
case "mongodb":
@@ -2963,10 +2990,7 @@ func installOptionalDriverAgentPackage(a *App, definition driverDefinition, sele
if strings.TrimSpace(downloadSource) == "" {
downloadSource = strings.TrimSpace(downloadURL)
}
agentRevision, revisionErr := verifyInstalledOptionalDriverAgentRevision(driverType, runtimePath, selectedVersion)
if revisionErr != nil {
return installedDriverPackage{}, revisionErr
}
agentRevision := observeInstalledOptionalDriverAgentRevision(driverType, runtimePath, selectedVersion)
return installedDriverPackage{
DriverType: driverType,
Version: strings.TrimSpace(selectedVersion),
@@ -3039,10 +3063,7 @@ func installOptionalDriverAgentFromLocalPath(definition driverDefinition, filePa
return installedDriverPackage{}, validateErr
}
agentRevision, revisionErr := verifyInstalledOptionalDriverAgentRevision(driverType, executablePath, selectedVersion)
if revisionErr != nil {
return installedDriverPackage{}, revisionErr
}
agentRevision := observeInstalledOptionalDriverAgentRevision(driverType, executablePath, selectedVersion)
hash, hashErr := hashFileSHA256(executablePath)
if hashErr != nil {
return installedDriverPackage{}, fmt.Errorf("计算 %s 驱动代理摘要失败:%w", displayName, hashErr)
@@ -3403,15 +3424,8 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut
if a != nil {
a.emitDriverDownloadProgress(driverType, "downloading", 10, 100, planMessage)
}
validateInstalledCandidateRevision := func() error {
if _, revisionErr := verifyInstalledOptionalDriverAgentRevision(driverType, executablePath, selectedVersion); revisionErr != nil {
_ = os.Remove(executablePath)
for _, supportName := range optionalDriverSupportFileNames(driverType) {
_ = os.Remove(filepath.Join(filepath.Dir(executablePath), supportName))
}
return revisionErr
}
return nil
observeInstalledCandidateRevision := func() {
observeInstalledOptionalDriverAgentRevision(driverType, executablePath, selectedVersion)
}
if !skipReuseCandidate {
if sourcePath, ok := findExistingOptionalDriverAgentCandidate(definition, executablePath); ok {
@@ -3426,9 +3440,7 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut
if hashErr != nil {
return "", "", fmt.Errorf("计算预置 %s 驱动代理摘要失败:%w", displayName, hashErr)
}
if revisionErr := validateInstalledCandidateRevision(); revisionErr != nil {
return "", "", fmt.Errorf("预置 %s 驱动代理 revision 校验失败(来源:%s%w", displayName, sourcePath, revisionErr)
}
observeInstalledCandidateRevision()
return "file://" + sourcePath, hash, nil
}
}
@@ -3463,11 +3475,7 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut
}
hash, dlErr := downloadOptionalDriverAgentBinary(a, definition, candidateURL, executablePath)
if dlErr == nil {
if revisionErr := validateInstalledCandidateRevision(); revisionErr != nil {
logger.Warnf("预编译 %s 驱动代理 revision 校验失败url=%s err=%v", displayName, candidateURL, revisionErr)
downloadErrs = appendOptionalDriverAttemptError(downloadErrs, candidateURL, revisionErr)
continue
}
observeInstalledCandidateRevision()
return candidateURL, hash, nil
}
logger.Warnf("下载预编译 %s 驱动代理失败url=%s err=%v", displayName, candidateURL, dlErr)
@@ -3486,11 +3494,7 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut
}
source, hash, bundleErr := downloadOptionalDriverAgentFromBundle(a, definition, bundleURL, executablePath)
if bundleErr == nil {
if revisionErr := validateInstalledCandidateRevision(); revisionErr != nil {
logger.Warnf("驱动总包 %s 代理 revision 校验失败source=%s err=%v", displayName, source, revisionErr)
downloadErrs = appendOptionalDriverAttemptError(downloadErrs, source, revisionErr)
continue
}
observeInstalledCandidateRevision()
return source, hash, nil
}
logger.Warnf("从驱动总包提取 %s 驱动代理失败url=%s err=%v", displayName, bundleURL, bundleErr)
@@ -4922,7 +4926,7 @@ func findExistingOptionalDriverAgentCandidate(definition driverDefinition, targe
if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, absPath); validateErr != nil {
continue
}
if !isReusableOptionalDriverAgentRevisionCurrent(driverType, absPath) {
if !isReusableOptionalDriverAgentCandidateRevisionAcceptable(driverType, absPath) {
continue
}
return absPath, true
@@ -4930,7 +4934,7 @@ func findExistingOptionalDriverAgentCandidate(definition driverDefinition, targe
return "", false
}
func isReusableOptionalDriverAgentRevisionCurrent(driverType string, executablePath string) bool {
func isReusableOptionalDriverAgentCandidateRevisionAcceptable(driverType string, executablePath string) bool {
expected := strings.TrimSpace(db.OptionalDriverAgentRevision(driverType))
if expected == "" {
return true
@@ -4938,12 +4942,16 @@ func isReusableOptionalDriverAgentRevisionCurrent(driverType string, executableP
actual, current, err := optionalDriverAgentRevisionCurrent(driverType, executablePath)
displayName := resolveDriverDisplayName(driverDefinition{Type: driverType})
if err != nil {
logger.Warnf("跳过可复用 %s 驱动代理候选版本元数据不可用 path=%s err=%v", displayName, executablePath, err)
return false
logger.Warnf("可复用 %s 驱动代理候选版本元数据不可用,仍允许安装:path=%s err=%v;建议在驱动管理中重装", displayName, executablePath, err)
return true
}
if !current {
logger.Warnf("跳过可复用 %s 驱动代理候选revision 不匹配 path=%s actual=%s expected=%s", displayName, executablePath, strings.TrimSpace(actual), expected)
return false
actualLabel := strings.TrimSpace(actual)
if actualLabel == "" {
actualLabel = "空"
}
logger.Warnf("可复用 %s 驱动代理候选 revision 不匹配仍允许安装path=%s actual=%s expected=%s建议在驱动管理中重装", displayName, executablePath, actualLabel, expected)
return true
}
return true
}

View File

@@ -538,6 +538,13 @@ func TestDownloadOptionalDriverAgentBinaryInstallsDuckDBDedicatedZip(t *testing.
http.ServeFile(w, r, zipPath)
}))
defer server.Close()
proxySnapshot := currentGlobalProxyConfig()
if _, err := setGlobalProxyConfig(false, proxySnapshot.Proxy); err != nil {
t.Fatalf("disable global proxy failed: %v", err)
}
t.Cleanup(func() {
_, _ = setGlobalProxyConfig(proxySnapshot.Enabled, proxySnapshot.Proxy)
})
target := filepath.Join(tmpDir, "install", "duckdb-driver-agent.exe")
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
@@ -916,12 +923,10 @@ func TestDownloadOptionalDriverAgentFromBundleSharesConcurrentDownload(t *testin
}
}
func TestEnsureOptionalDriverAgentBinaryFallsBackAfterStaleDownloadRevision(t *testing.T) {
func TestInstallOptionalDriverAgentPackageAcceptsStaleDownloadRevision(t *testing.T) {
originalProbe := optionalDriverAgentMetadataProbe
originalGoBinaryLookPath := goBinaryLookPath
t.Cleanup(func() {
optionalDriverAgentMetadataProbe = originalProbe
goBinaryLookPath = originalGoBinaryLookPath
})
tmpDir := t.TempDir()
@@ -935,73 +940,46 @@ func TestEnsureOptionalDriverAgentBinaryFallsBackAfterStaleDownloadRevision(t *t
http.ServeFile(w, r, staleAgent)
}))
defer staleServer.Close()
projectRoot := filepath.Join(tmpDir, "project")
if err := os.MkdirAll(filepath.Join(projectRoot, "cmd", "optional-driver-agent"), 0o755); err != nil {
t.Fatalf("create project root failed: %v", err)
}
if err := os.WriteFile(filepath.Join(projectRoot, "go.mod"), []byte("module GoNavi-Wails\n"), 0o644); err != nil {
t.Fatalf("write go.mod failed: %v", err)
}
if err := os.WriteFile(filepath.Join(projectRoot, "cmd", "optional-driver-agent", "main.go"), []byte("package main\n"), 0o644); err != nil {
t.Fatalf("write optional agent main failed: %v", err)
}
wd, err := os.Getwd()
if err != nil {
t.Fatalf("getwd failed: %v", err)
}
if err := os.Chdir(projectRoot); err != nil {
t.Fatalf("chdir project root failed: %v", err)
proxySnapshot := currentGlobalProxyConfig()
if _, err := setGlobalProxyConfig(false, proxySnapshot.Proxy); err != nil {
t.Fatalf("disable global proxy failed: %v", err)
}
t.Cleanup(func() {
if err := os.Chdir(wd); err != nil {
t.Fatalf("restore cwd failed: %v", err)
}
_, _ = setGlobalProxyConfig(proxySnapshot.Enabled, proxySnapshot.Proxy)
})
goScript := filepath.Join(tmpDir, "fake-go")
if runtime.GOOS == "windows" {
goScript += ".bat"
}
if runtime.GOOS == "windows" {
if err := os.WriteFile(goScript, []byte("@echo off\r\nsetlocal\r\nset \"out=\"\r\n:loop\r\nif \"%~1\"==\"\" goto done\r\nif \"%~1\"==\"-o\" goto capture\r\nshift\r\ngoto loop\r\n:capture\r\nset \"out=%~2\"\r\nshift\r\nshift\r\ngoto loop\r\n:done\r\nif \"%out%\"==\"\" exit /b 1\r\ncopy /Y \"%GONAVI_TEST_BUILT_AGENT%\" \"%out%\" >nul\r\n"), 0o755); err != nil {
t.Fatalf("write fake go script failed: %v", err)
}
} else {
if err := os.WriteFile(goScript, []byte("#!/usr/bin/env sh\nout=\"\"\nwhile [ \"$#\" -gt 0 ]; do\n if [ \"$1\" = \"-o\" ]; then out=\"$2\"; shift 2; continue; fi\n shift\ndone\ncp \"$GONAVI_TEST_BUILT_AGENT\" \"$out\"\n"), 0o755); err != nil {
t.Fatalf("write fake go script failed: %v", err)
}
}
goBinaryLookPath = func(file string) (string, error) {
return goScript, nil
}
t.Setenv("GONAVI_TEST_BUILT_AGENT", staleAgent)
probeCount := 0
optionalDriverAgentMetadataProbe = func(driverType string, executablePath string) (db.OptionalDriverAgentMetadata, error) {
probeCount++
revision := "src-stale-agent"
if probeCount > 1 {
revision = db.OptionalDriverAgentRevision(driverType)
}
return db.OptionalDriverAgentMetadata{
DriverType: driverType,
AgentRevision: revision,
AgentRevision: "src-stale-agent",
}, nil
}
targetPath := filepath.Join(tmpDir, optionalDriverExecutableBaseName("sqlserver"))
source, _, err := ensureOptionalDriverAgentBinary(
meta, err := installOptionalDriverAgentPackage(
nil,
driverDefinition{Type: "sqlserver", Name: "SQL Server"},
targetPath,
staleServer.URL,
"1.9.6",
filepath.Join(tmpDir, "drivers"),
staleServer.URL,
)
if err != nil {
t.Fatalf("expected stale direct download to fall back to source build, got %v", err)
t.Fatalf("expected stale direct download to be installed with an update hint, got %v", err)
}
if source != "local://go-build/sqlserver-driver-agent" {
t.Fatalf("expected source build fallback, got %q", source)
if meta.DownloadURL != staleServer.URL {
t.Fatalf("expected direct download source to be preserved, got %q", meta.DownloadURL)
}
if meta.AgentRevision != "src-stale-agent" {
t.Fatalf("expected stale agent revision to be recorded, got %q", meta.AgentRevision)
}
if _, err := os.Stat(meta.ExecutablePath); err != nil {
t.Fatalf("expected runtime executable to stay installed, got %v", err)
}
needsUpdate, reason, expectedRevision := optionalDriverAgentRevisionStatus("sqlserver", meta, true)
if !needsUpdate {
t.Fatalf("expected stale installed revision to be surfaced as needsUpdate; expected=%q", expectedRevision)
}
if !strings.Contains(reason, "强烈建议重装") {
t.Fatalf("expected advisory reinstall reason, got %q", reason)
}
}