feat(sqlserver): add explicit legacy driver selection

This commit is contained in:
zipg 2026-07-22 23:02:08 +08:00 committed by GitHub
parent 5d1a6e63cd
commit 22ccdef594
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 228 additions and 155 deletions

View File

@ -50,7 +50,7 @@ import { normalizeKafkaBootstrapServers } from "@/lib/connection/kafkaBootstrapS
import { normalizeRocketmqNamesrvAddr } from "@/lib/connection/rocketmqNamesrv";
import { detectMqUiAuthKind, isMqAuthKindAllowedForSystem, type MqUiAuthKind } from "@/lib/connection/mqAuth";
import { driverInstallProgressPercent, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
import { isSqlServerLegacyCompatibilityMode, requiresSqlServerLegacyCompatibilityComponent, setSqlServerLegacyCompatibilityMode, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility";
import { requiresSqlServerLegacyCompatibilityComponent, setSqlServerLegacyCompatibilityConfig, sqlServerUsesLegacyCompatibility, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility";
import {
ArrowLeft,
ArrowDown,
@ -1330,33 +1330,24 @@ async function installSqlServerLegacyCompatibilityComponentIfNeeded(): Promise<b
return true;
}
async function onSqlServerLegacyCompatibilityModeChange(event: Event) {
async function setSqlServerDriverMode(mode: "auto" | "legacy") {
if (form.value.db_type !== "sqlserver") return;
const input = event.target instanceof HTMLInputElement ? event.target : null;
const enabled = input?.checked === true;
// The connection test may still be using the previous compatibility mode.
resetTestState();
if (!enabled) {
form.value.url_params = setSqlServerLegacyCompatibilityMode(form.value.url_params, false);
if (mode === "auto") {
setSqlServerLegacyCompatibilityConfig(form.value, false);
return;
}
if (input) input.checked = false;
try {
await installSqlServerLegacyCompatibilityComponentIfNeeded();
form.value.url_params = setSqlServerLegacyCompatibilityMode(form.value.url_params, true);
setSqlServerLegacyCompatibilityConfig(form.value, true);
testResult.value = null;
} catch {
form.value.url_params = setSqlServerLegacyCompatibilityMode(form.value.url_params, false);
if (input) input.checked = false;
setSqlServerLegacyCompatibilityConfig(form.value, false);
}
}
function isSqlServerTlsHandshakeFailure(message: string): boolean {
const text = message.toLowerCase();
return text.includes("sql server") && text.includes("tls") && (text.includes("handshake") || text.includes("eof") || text.includes("performing i/o"));
}
function clearTestedConnectionInfo() {
testedConfigFingerprint.value = "";
testedConfigId.value = "";
@ -2401,7 +2392,7 @@ const agentInstallProgressLabel = computed(() => {
return `${label} ${formatInstallSize(progress.downloaded ?? 0)} / ${formatInstallSize(progress.total)} (${agentInstallPercent.value ?? 0}%)`;
});
const canCloseAgentInstallDialog = computed(() => !agentInstallRunning.value || !!agentInstallError.value);
const sqlServerLegacyCompatibilityModeEnabled = computed(() => form.value.db_type === "sqlserver" && isSqlServerLegacyCompatibilityMode(form.value.url_params));
const sqlServerDriverMode = computed<"auto" | "legacy">(() => (sqlServerUsesLegacyCompatibility(form.value) ? "legacy" : "auto"));
const shouldUseWideConnectionDialog = computed(() => dialogStep.value === "config" && (canChooseVisibleDatabases.value || (canChooseVisibleSchemas.value && !visibleFilterUsesSchemas.value)));
const connectionDialogContentClass = computed(() => {
if (dialogStep.value === "select") return "sm:max-w-[760px]";
@ -2505,10 +2496,6 @@ async function testConnection() {
const message = config ? connectionErrorWithDriverUpdateHint(config, rawMessage) : rawMessage;
const fallback = config ? await tryNacosDockerConsoleFallback(config, message, runId) : null;
if (runId !== testRunId) return;
const shouldShowSqlServerLegacyMode = !fallback && config?.db_type === "sqlserver" && !isSqlServerLegacyCompatibilityMode(config.url_params) && isSqlServerTlsHandshakeFailure(message);
if (shouldShowSqlServerLegacyMode) {
configTab.value = "advanced";
}
if (fallback) {
applySuccessfulConnectionTest(fallback.result, fallback.config, submittedSourceName);
void persistSuccessfulConnectionTest(fallback.result, fallback.config, submittedSourceName, runId);
@ -5236,6 +5223,22 @@ function openExternalUrl(url: string) {
</div>
</div>
<div v-if="form.db_type === 'sqlserver'" class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.driverMode") }}</Label>
<div class="col-span-3 flex items-center gap-2">
<Button size="sm" :variant="sqlServerDriverMode === 'legacy' ? 'outline' : 'default'" :disabled="agentInstallRunning" @click="setSqlServerDriverMode('auto')">{{ t("connection.mongoDriverAuto") }}</Button>
<Button size="sm" :variant="sqlServerDriverMode === 'legacy' ? 'default' : 'outline'" :disabled="agentInstallRunning" @click="setSqlServerDriverMode('legacy')">{{ t("connection.mongoDriverLegacy") }}</Button>
<Tooltip>
<TooltipTrigger as-child>
<CircleHelp class="h-3.5 w-3.5 cursor-help text-muted-foreground hover:text-foreground" />
</TooltipTrigger>
<TooltipContent side="top" align="center" class="max-w-[320px] whitespace-pre-line text-xs leading-relaxed">
{{ t("connection.sqlServerLegacyCompatibilityModeHint") }}
</TooltipContent>
</Tooltip>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">{{ form.db_type === "elasticsearch" && elasticsearchConnectionMode === "kibana" ? t("connection.elasticsearchKibanaHost") : t("connection.host") }}</Label>
<Input v-model="form.host" class="col-span-2" />
@ -5838,18 +5841,6 @@ function openExternalUrl(url: string) {
</template>
</div>
</div>
<div v-show="form.db_type === 'sqlserver'" class="grid grid-cols-4 items-start gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.sqlServerLegacyCompatibilityMode") }}</Label>
<div class="col-span-3 flex flex-col gap-1">
<label class="flex h-5 cursor-pointer items-center gap-2">
<input type="checkbox" :checked="sqlServerLegacyCompatibilityModeEnabled" :disabled="agentInstallRunning" class="mr-0" @change="onSqlServerLegacyCompatibilityModeChange" />
<span class="text-xs text-foreground">{{ t("connection.sqlServerLegacyCompatibilityModeEnable") }}</span>
</label>
<p class="m-0 whitespace-pre-line text-xs leading-5 text-muted-foreground">
{{ t("connection.sqlServerLegacyCompatibilityModeHint") }}
</p>
</div>
</div>
<div v-show="form.db_type === 'redis'" class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("settings.redisScanPageSize") }}</Label>
<div class="col-span-3 flex flex-col gap-1">

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { isSqlServerLegacyCompatibilityMode, requiresSqlServerLegacyCompatibilityComponent, setSqlServerLegacyCompatibilityMode } from "@/lib/connection/sqlServerLegacyCompatibility";
import { isSqlServerNativeEncryptionDisabled, requiresSqlServerLegacyCompatibilityComponent, setSqlServerLegacyCompatibilityConfig, setSqlServerNativeEncryptionDisabled, sqlServerUsesLegacyCompatibility } from "@/lib/connection/sqlServerLegacyCompatibility";
import type { ConnectionConfig } from "@/types/database";
function connectionConfig(urlParams?: string): ConnectionConfig {
@ -25,26 +25,51 @@ function connectionConfig(urlParams?: string): ConnectionConfig {
}
describe("SQL Server legacy compatibility", () => {
it("treats existing disabled encryption params as legacy compatibility opt-in", () => {
expect(isSqlServerLegacyCompatibilityMode("sqlserverEncryption=disabled")).toBe(true);
expect(isSqlServerLegacyCompatibilityMode("applicationName=dbx;encrypt=false")).toBe(true);
expect(isSqlServerLegacyCompatibilityMode("?Encrypt=0&applicationName=dbx")).toBe(true);
expect(isSqlServerLegacyCompatibilityMode("encrypt=true")).toBe(false);
it("recognizes native encryption policy independently from the legacy driver profile", () => {
expect(isSqlServerNativeEncryptionDisabled("sqlserverEncryption=disabled")).toBe(true);
expect(isSqlServerNativeEncryptionDisabled("applicationName=dbx;encrypt=false")).toBe(true);
expect(isSqlServerNativeEncryptionDisabled("?Encrypt=0&applicationName=dbx")).toBe(true);
expect(isSqlServerNativeEncryptionDisabled("encrypt=true")).toBe(false);
});
it("updates URL params without keeping conflicting encryption params", () => {
expect(setSqlServerLegacyCompatibilityMode("applicationName=dbx;encrypt=true", true)).toBe("applicationName=dbx&sqlserverEncryption=disabled");
expect(setSqlServerLegacyCompatibilityMode("applicationName=dbx;sqlserverEncryption=disabled", false)).toBe("applicationName=dbx");
it("updates native encryption params without changing the driver profile", () => {
expect(setSqlServerNativeEncryptionDisabled("applicationName=dbx;encrypt=true", true)).toBe("applicationName=dbx&sqlserverEncryption=disabled");
expect(setSqlServerNativeEncryptionDisabled("applicationName=dbx;sqlserverEncryption=disabled", false)).toBe("applicationName=dbx");
});
it("requires the hidden component only for SQL Server legacy compatibility connections", () => {
expect(requiresSqlServerLegacyCompatibilityComponent(connectionConfig("sqlserverEncryption=disabled"))).toBe(true);
expect(requiresSqlServerLegacyCompatibilityComponent(connectionConfig("encrypt=true"))).toBe(false);
it("keeps historical disabled-encryption connections on the native driver", () => {
const config = connectionConfig("sqlserverEncryption=disabled");
expect(sqlServerUsesLegacyCompatibility(config)).toBe(false);
expect(requiresSqlServerLegacyCompatibilityComponent(config)).toBe(false);
expect(
requiresSqlServerLegacyCompatibilityComponent({
...connectionConfig("sqlserverEncryption=disabled"),
...config,
driver_profile: "sqlserver-legacy",
db_type: "mysql",
}),
).toBe(false);
});
it("treats a persisted legacy driver profile as compatibility mode", () => {
const config = connectionConfig("");
config.driver_profile = "sqlserver-legacy";
expect(sqlServerUsesLegacyCompatibility(config)).toBe(true);
expect(requiresSqlServerLegacyCompatibilityComponent(config)).toBe(true);
});
it("updates the explicit driver profile without rewriting native encryption params", () => {
const config = connectionConfig("applicationName=dbx&encrypt=false");
setSqlServerLegacyCompatibilityConfig(config, true);
expect(config.driver_profile).toBe("sqlserver-legacy");
expect(config.driver_label).toBe("SQL Server legacy compatibility component");
expect(config.url_params).toBe("applicationName=dbx&encrypt=false");
setSqlServerLegacyCompatibilityConfig(config, false);
expect(config.driver_profile).toBe("sqlserver");
expect(config.driver_label).toBe("SQL Server");
expect(config.url_params).toBe("applicationName=dbx&encrypt=false");
});
});

View File

@ -1,34 +1,45 @@
import type { ConnectionConfig } from "@/types/database";
export const SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY = "sqlserver-legacy";
export const SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_LABEL = "SQL Server legacy compatibility component";
export const SQLSERVER_NATIVE_DRIVER_PROFILE = "sqlserver";
export const SQLSERVER_NATIVE_DRIVER_LABEL = "SQL Server";
const SQLSERVER_LEGACY_DISABLED_VALUES = new Set(["disabled", "disable", "false", "0", "off", "no"]);
const SQLSERVER_ENCRYPTION_DISABLED_VALUES = new Set(["disabled", "disable", "false", "0", "off", "no"]);
export function isSqlServerLegacyCompatibilityMode(params: string | undefined): boolean {
export function isSqlServerNativeEncryptionDisabled(params: string | undefined): boolean {
const normalized = (params || "").trim().replace(/^\?/, "").replace(/;/g, "&");
if (!normalized) return false;
const parsed = new URLSearchParams(normalized);
for (const [key, value] of parsed.entries()) {
const normalizedKey = key.trim().toLowerCase();
if (normalizedKey === "sqlserverencryption" || normalizedKey === "encrypt") {
// Accept JDBC-style `encrypt=false` from imported SQL Server URLs as the same opt-in.
if (SQLSERVER_LEGACY_DISABLED_VALUES.has(value.trim().toLowerCase())) return true;
if (SQLSERVER_ENCRYPTION_DISABLED_VALUES.has(value.trim().toLowerCase())) return true;
}
}
return false;
}
export function setSqlServerLegacyCompatibilityMode(params: string | undefined, enabled: boolean): string {
export function setSqlServerNativeEncryptionDisabled(params: string | undefined, disabled: boolean): string {
const normalized = (params || "").trim().replace(/^\?/, "").replace(/;/g, "&");
const parsed = new URLSearchParams(normalized);
for (const key of Array.from(parsed.keys())) {
const normalizedKey = key.trim().toLowerCase();
if (normalizedKey === "sqlserverencryption" || normalizedKey === "encrypt") parsed.delete(key);
}
if (enabled) parsed.set("sqlserverEncryption", "disabled");
if (disabled) parsed.set("sqlserverEncryption", "disabled");
return parsed.toString();
}
export function requiresSqlServerLegacyCompatibilityComponent(config: ConnectionConfig): boolean {
return config.db_type === "sqlserver" && isSqlServerLegacyCompatibilityMode(config.url_params);
export function sqlServerUsesLegacyCompatibility(config: Pick<ConnectionConfig, "db_type" | "driver_profile">): boolean {
return config.db_type === "sqlserver" && config.driver_profile === SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY;
}
export function setSqlServerLegacyCompatibilityConfig(config: Pick<ConnectionConfig, "driver_label" | "driver_profile">, enabled: boolean): void {
config.driver_profile = enabled ? SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY : SQLSERVER_NATIVE_DRIVER_PROFILE;
config.driver_label = enabled ? SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_LABEL : SQLSERVER_NATIVE_DRIVER_LABEL;
}
export function requiresSqlServerLegacyCompatibilityComponent(config: ConnectionConfig): boolean {
return sqlServerUsesLegacyCompatibility(config);
}

View File

@ -11,7 +11,7 @@ function installLocalStorage() {
});
}
function sqlServerLegacyConnection(): ConnectionConfig {
function sqlServerNativeConnectionWithDisabledEncryption(): ConnectionConfig {
return {
id: "sqlserver-1",
name: "SQL Server",
@ -41,7 +41,7 @@ describe("connectionStore SQL Server legacy compatibility", () => {
setActivePinia(createPinia());
});
it("installs the legacy compatibility component before connecting saved legacy configs", async () => {
it("does not preinstall the legacy component for historical disabled-encryption configs", async () => {
const connectDb = vi.fn().mockResolvedValue("sqlserver-1");
const installAgent = vi.fn().mockResolvedValue(undefined);
@ -54,10 +54,53 @@ describe("connectionStore SQL Server legacy compatibility", () => {
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
await store.connect(sqlServerLegacyConnection());
await store.connect(sqlServerNativeConnectionWithDisabledEncryption());
expect(installAgent).toHaveBeenCalledWith("sqlserver-legacy");
expect(installAgent).not.toHaveBeenCalled();
expect(connectDb).toHaveBeenCalledTimes(1);
expect(installAgent.mock.invocationCallOrder[0]).toBeLessThan(connectDb.mock.invocationCallOrder[0]);
});
it("reconnects persisted legacy profiles without reinstalling an installed component", async () => {
const connectDb = vi.fn().mockResolvedValue("sqlserver-1");
const installAgent = vi.fn().mockResolvedValue(undefined);
const config = sqlServerNativeConnectionWithDisabledEncryption();
config.driver_profile = "sqlserver-legacy";
config.driver_label = "SQL Server legacy compatibility component";
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => true }));
vi.doMock("@/lib/backend/api", () => ({
connectDb,
installAgent,
isAgentInstalled: vi.fn().mockResolvedValue(true),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
await store.connect(config);
expect(installAgent).not.toHaveBeenCalled();
expect(connectDb).toHaveBeenCalledOnce();
expect(connectDb).toHaveBeenCalledWith(expect.objectContaining({ driver_profile: "sqlserver-legacy" }), expect.any(Number));
});
it("does not install or retry legacy after a TLS error followed by SQL Server 18456", async () => {
const connectDb = vi.fn().mockRejectedValue(new Error("TLS negotiation failed\nSQL Server error 18456: Login failed"));
const installAgent = vi.fn().mockResolvedValue(undefined);
const config = sqlServerNativeConnectionWithDisabledEncryption();
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => true }));
vi.doMock("@/lib/backend/api", () => ({
connectDb,
installAgent,
isAgentInstalled: vi.fn().mockResolvedValue(false),
listInstalledAgents: vi.fn().mockResolvedValue([]),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
await expect(store.connect(config)).rejects.toThrow("18456");
expect(connectDb).toHaveBeenCalledOnce();
expect(installAgent).not.toHaveBeenCalled();
});
});

View File

@ -2046,7 +2046,9 @@ export const useConnectionStore = defineStore("connection", () => {
const localAttempt = beginLocalConnectionAttempt(config.id);
try {
await beforeConnectHandler?.(config);
await ensureSqlServerLegacyCompatibilityComponentInstalled(config);
if (config.db_type === "sqlserver") {
await ensureSqlServerLegacyCompatibilityComponentInstalled(config);
}
ensureLocalConnectionAttemptActive(config.id, localAttempt);
const id = await withConnectionAttemptTimeout(api.connectDb(config, localAttempt), config);
await ensureLocalConnectionAttemptActiveAfterConnectResult(config.id, localAttempt, id);
@ -2213,6 +2215,9 @@ export const useConnectionStore = defineStore("connection", () => {
const localAttempt = beginLocalConnectionAttempt(connectionId);
const connectPromise = (async () => {
await beforeConnectHandler?.(config);
if (config.db_type === "sqlserver") {
await ensureSqlServerLegacyCompatibilityComponentInstalled(config);
}
ensureLocalConnectionAttemptActive(connectionId, localAttempt);
const id = await withConnectionAttemptTimeout(api.connectDb(config, localAttempt), config);
await ensureLocalConnectionAttemptActiveAfterConnectResult(connectionId, localAttempt, id);

View File

@ -333,15 +333,21 @@ pub fn sqlserver_legacy_agent_config(config: &ConnectionConfig) -> ConnectionCon
legacy_config
}
pub fn sqlserver_legacy_agent_error(native_error: &str, agent_error: &str) -> String {
let install_hint = if agent_error.contains("driver is not installed") {
format!("\n\n{SQLSERVER_LEGACY_DRIVER_INSTALL_HINT}")
pub fn sqlserver_uses_legacy_driver(config: &ConnectionConfig) -> bool {
config
.driver_profile
.as_deref()
.is_some_and(|profile| profile.eq_ignore_ascii_case(db::sqlserver::SQLSERVER_LEGACY_DRIVER_PROFILE))
}
pub fn sqlserver_legacy_driver_error(agent_error: &str) -> String {
// AgentManager currently returns launch failures as strings. This exact marker is generated
// internally and only adds guidance for an explicitly selected compatibility driver.
if agent_error.contains("driver is not installed") {
format!("{agent_error}\n\n{SQLSERVER_LEGACY_DRIVER_INSTALL_HINT}")
} else {
String::new()
};
format!(
"{native_error}\n\nFallback with SQL Server legacy compatibility component failed: {agent_error}{install_hint}"
)
agent_error.to_string()
}
}
pub async fn connect_mysql_metadata_pool(
@ -748,26 +754,48 @@ impl AppState {
Ok(PoolKind::ExternalDriver { driver_id: driver_id.to_string(), config: Arc::new(config.clone()), session })
}
pub async fn test_sqlserver_connection_with_legacy_fallback(
pub async fn test_sqlserver_connection(
&self,
config: &ConnectionConfig,
host: &str,
port: u16,
connect_timeout: Duration,
) -> Result<String, String> {
self.test_sqlserver_connection_with_legacy_fallback_with_info(config, host, port, connect_timeout)
.await
.map(|result| result.message)
self.test_sqlserver_connection_with_info(config, host, port, connect_timeout).await.map(|result| result.message)
}
pub async fn test_sqlserver_connection_with_legacy_fallback_with_info(
pub async fn test_sqlserver_connection_with_info(
&self,
config: &ConnectionConfig,
host: &str,
port: u16,
connect_timeout: Duration,
) -> Result<ConnectionTestResult, String> {
match db::sqlserver::connect_with_port_explicit(
if sqlserver_uses_legacy_driver(config) {
let legacy_config = sqlserver_legacy_agent_config(config);
let connect_params =
agent_connect_params(&legacy_config, host, port, legacy_config.effective_database().unwrap_or(""));
let mut client = self
.agent_manager
.spawn(&legacy_config.db_type, legacy_config.driver_profile.as_deref())
.await
.map_err(|err| sqlserver_legacy_driver_error(&err))?;
let response = client
.call_method_with_timeout::<serde_json::Value>(
AgentMethod::TestConnection,
connect_params,
Some(agent_connect_timeout(&legacy_config)),
)
.await
.map_err(|err| sqlserver_legacy_driver_error(&err))?;
client.disconnect().await.ok();
return Ok(ConnectionTestResult::success(
"Connection successful (via SQL Server legacy compatibility driver)",
)
.with_database_info(database_info_from_protocol_value(&response)));
}
db::sqlserver::connect_with_port_explicit(
host,
port,
config.sqlserver_port_explicit(),
@ -776,44 +804,38 @@ impl AppState {
config.database.as_deref(),
connect_timeout,
)
.await
{
Ok(_) => Ok(ConnectionTestResult::success("Connection successful")),
Err(native_error)
if db::sqlserver::sqlserver_legacy_compatibility_enabled(config.url_params.as_deref()) =>
{
let legacy_config = sqlserver_legacy_agent_config(config);
let connect_params =
agent_connect_params(&legacy_config, host, port, legacy_config.effective_database().unwrap_or(""));
let mut client = self
.agent_manager
.spawn(&legacy_config.db_type, legacy_config.driver_profile.as_deref())
.await
.map_err(|err| sqlserver_legacy_agent_error(&native_error, &err))?;
let response = client
.call_method_with_timeout::<serde_json::Value>(
AgentMethod::TestConnection,
connect_params,
Some(agent_connect_timeout(&legacy_config)),
)
.await
.map_err(|err| sqlserver_legacy_agent_error(&native_error, &err))?;
client.disconnect().await.ok();
Ok(ConnectionTestResult::success("Connection successful (via SQL Server legacy compatibility driver)")
.with_database_info(database_info_from_protocol_value(&response)))
}
Err(err) => Err(err),
}
.await?;
Ok(ConnectionTestResult::success("Connection successful"))
}
pub async fn connect_sqlserver_pool_with_legacy_fallback(
pub async fn connect_sqlserver_pool(
&self,
config: &ConnectionConfig,
host: &str,
port: u16,
connect_timeout: Duration,
) -> Result<PoolKind, String> {
match db::sqlserver::connect_with_port_explicit(
if sqlserver_uses_legacy_driver(config) {
let legacy_config = sqlserver_legacy_agent_config(config);
let connect_params =
agent_connect_params(&legacy_config, host, port, legacy_config.effective_database().unwrap_or(""));
let mut client = self
.agent_manager
.spawn(&legacy_config.db_type, legacy_config.driver_profile.as_deref())
.await
.map_err(|err| sqlserver_legacy_driver_error(&err))?;
client
.call_method_with_timeout::<serde_json::Value>(
AgentMethod::Connect,
connect_params,
Some(agent_connect_timeout(&legacy_config)),
)
.await
.map_err(|err| sqlserver_legacy_driver_error(&err))?;
return Ok(PoolKind::Agent(Arc::new(tokio::sync::Mutex::new(client))));
}
let client = db::sqlserver::connect_with_port_explicit(
host,
port,
config.sqlserver_port_explicit(),
@ -822,32 +844,8 @@ impl AppState {
config.database.as_deref(),
connect_timeout,
)
.await
{
Ok(client) => Ok(PoolKind::SqlServer(Arc::new(tokio::sync::Mutex::new(client)))),
Err(native_error)
if db::sqlserver::sqlserver_legacy_compatibility_enabled(config.url_params.as_deref()) =>
{
let legacy_config = sqlserver_legacy_agent_config(config);
let connect_params =
agent_connect_params(&legacy_config, host, port, legacy_config.effective_database().unwrap_or(""));
let mut client = self
.agent_manager
.spawn(&legacy_config.db_type, legacy_config.driver_profile.as_deref())
.await
.map_err(|err| sqlserver_legacy_agent_error(&native_error, &err))?;
client
.call_method_with_timeout::<serde_json::Value>(
AgentMethod::Connect,
connect_params,
Some(agent_connect_timeout(&legacy_config)),
)
.await
.map_err(|err| sqlserver_legacy_agent_error(&native_error, &err))?;
Ok(PoolKind::Agent(Arc::new(tokio::sync::Mutex::new(client))))
}
Err(err) => Err(err),
}
.await?;
Ok(PoolKind::SqlServer(Arc::new(tokio::sync::Mutex::new(client))))
}
pub fn external_driver_runtime_env(&self, driver_id: &str) -> Result<PluginRuntimeEnv, String> {
@ -1381,9 +1379,7 @@ impl AppState {
db::clickhouse_driver::test_connection(&client, connect_timeout).await?;
PoolKind::ClickHouse(client)
}
DatabaseType::SqlServer => {
self.connect_sqlserver_pool_with_legacy_fallback(&db_config, &host, port, connect_timeout).await?
}
DatabaseType::SqlServer => self.connect_sqlserver_pool(&db_config, &host, port, connect_timeout).await?,
DatabaseType::Elasticsearch => {
let mut client = db::elasticsearch_driver::EsClient::from_config(
&url,
@ -3753,8 +3749,8 @@ mod tests {
metadata_connection_config, mysql_metadata_fallback_url, oceanbase_mysql_query_timeout_sql,
oceanbase_mysql_setup_queries, prestosql_jdbc_config_for_endpoint, redacted_connection_url_for_endpoint,
redis_sentinel_transport_id, redis_sentinel_transport_prefix, sqlserver_legacy_agent_config,
sqlserver_legacy_agent_error, task_client_session_id, uses_bare_mysql_pool, uses_tcp_probe,
validate_h2_database_path, AppState, MysqlMode, PoolKind, PRESTOSQL_JDBC_DRIVER_CLASS,
sqlserver_legacy_driver_error, sqlserver_uses_legacy_driver, task_client_session_id, uses_bare_mysql_pool,
uses_tcp_probe, validate_h2_database_path, AppState, MysqlMode, PoolKind, PRESTOSQL_JDBC_DRIVER_CLASS,
};
use crate::agent_connection::{
agent_connect_params, mongo_legacy_error_with_auth_hint, mongo_uses_legacy_driver,
@ -3891,17 +3887,24 @@ mod tests {
assert_eq!(legacy.db_type, DatabaseType::SqlServer);
assert_eq!(legacy.driver_profile.as_deref(), Some(crate::db::sqlserver::SQLSERVER_LEGACY_DRIVER_PROFILE));
assert_eq!(legacy.driver_label.as_deref(), Some(crate::db::sqlserver::SQLSERVER_LEGACY_DRIVER_LABEL));
assert!(sqlserver_uses_legacy_driver(&legacy));
}
#[test]
fn sqlserver_legacy_agent_error_mentions_driver_manager_when_missing() {
let message = sqlserver_legacy_agent_error(
"native failed",
fn sqlserver_legacy_url_param_does_not_force_agent_driver() {
let mut config = mysql_config(Some("master"));
config.db_type = DatabaseType::SqlServer;
config.url_params = Some("applicationName=dbx;encrypt=false".to_string());
assert!(!sqlserver_uses_legacy_driver(&config));
}
#[test]
fn sqlserver_legacy_driver_error_mentions_driver_manager_when_missing() {
let message = sqlserver_legacy_driver_error(
"sqlserver-legacy driver is not installed. Please install it from the Driver Manager.",
);
assert!(message.contains("native failed"));
assert!(message.contains("Fallback with SQL Server legacy compatibility component failed"));
assert!(message.contains("Driver Manager"));
assert!(message.contains("enable SQL Server legacy compatibility mode again"));
}

View File

@ -177,7 +177,7 @@ async fn try_connect_legacy_sqlserver_encryption(
Err(errors.join("\n"))
}
pub fn sqlserver_legacy_compatibility_enabled(url_params: Option<&str>) -> bool {
pub fn sqlserver_native_encryption_disabled(url_params: Option<&str>) -> bool {
let Some(params) = url_params.map(str::trim).filter(|params| !params.is_empty()) else {
return false;
};
@ -2355,14 +2355,14 @@ mod tests {
}
#[test]
fn sqlserver_legacy_compatibility_flag_accepts_dbx_and_jdbc_params() {
assert!(!super::sqlserver_legacy_compatibility_enabled(None));
assert!(!super::sqlserver_legacy_compatibility_enabled(Some("encrypt=true")));
assert!(super::sqlserver_legacy_compatibility_enabled(Some("sqlserverEncryption=disabled")));
assert!(super::sqlserver_legacy_compatibility_enabled(Some("applicationName=dbx;sqlserverEncryption=off")));
assert!(super::sqlserver_legacy_compatibility_enabled(Some("?sqlserverEncryption=false&applicationName=dbx")));
assert!(super::sqlserver_legacy_compatibility_enabled(Some("applicationName=dbx;encrypt=false")));
assert!(super::sqlserver_legacy_compatibility_enabled(Some("?Encrypt=0&applicationName=dbx")));
fn sqlserver_native_encryption_flag_accepts_dbx_and_jdbc_params() {
assert!(!super::sqlserver_native_encryption_disabled(None));
assert!(!super::sqlserver_native_encryption_disabled(Some("encrypt=true")));
assert!(super::sqlserver_native_encryption_disabled(Some("sqlserverEncryption=disabled")));
assert!(super::sqlserver_native_encryption_disabled(Some("applicationName=dbx;sqlserverEncryption=off")));
assert!(super::sqlserver_native_encryption_disabled(Some("?sqlserverEncryption=false&applicationName=dbx")));
assert!(super::sqlserver_native_encryption_disabled(Some("applicationName=dbx;encrypt=false")));
assert!(super::sqlserver_native_encryption_disabled(Some("?Encrypt=0&applicationName=dbx")));
}
#[test]

View File

@ -897,10 +897,7 @@ async fn test_connection_with_info_inner(
.map(|_| "Connection successful".to_string())
}
DatabaseType::SqlServer => {
match state
.test_sqlserver_connection_with_legacy_fallback_with_info(&config, &host, port, connect_timeout)
.await
{
match state.test_sqlserver_connection_with_info(&config, &host, port, connect_timeout).await {
Ok(details) => {
database_info = details.database_info;
Ok(details.message)
@ -1229,9 +1226,7 @@ pub async fn connect_db(
db::clickhouse_driver::test_connection(&client, connect_timeout).await?;
PoolKind::ClickHouse(client)
}
DatabaseType::SqlServer => {
state.connect_sqlserver_pool_with_legacy_fallback(&db_config, &host, port, connect_timeout).await?
}
DatabaseType::SqlServer => state.connect_sqlserver_pool(&db_config, &host, port, connect_timeout).await?,
DatabaseType::Elasticsearch => {
let mut client = db::elasticsearch_driver::EsClient::from_config(
&url,