feat(sqlserver): add explicit legacy driver selection
This commit is contained in:
parent
5d1a6e63cd
commit
22ccdef594
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue