fix(sqlserver): preserve explicit instance ports
This commit is contained in:
parent
8e87ab2e39
commit
8020fe5886
|
|
@ -12,6 +12,7 @@ public final class ConnectParams {
|
|||
private String password;
|
||||
private String url_params;
|
||||
private String connection_string;
|
||||
private boolean port_explicit;
|
||||
private boolean mysql_compat_mode;
|
||||
private String jdbc_driver_class;
|
||||
private List<String> jdbc_driver_paths;
|
||||
|
|
@ -91,6 +92,10 @@ public final class ConnectParams {
|
|||
return connection_string;
|
||||
}
|
||||
|
||||
public boolean isPort_explicit() {
|
||||
return port_explicit;
|
||||
}
|
||||
|
||||
public void setHost(String host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
|
@ -119,6 +124,10 @@ public final class ConnectParams {
|
|||
this.connection_string = connection_string;
|
||||
}
|
||||
|
||||
public void setPort_explicit(boolean port_explicit) {
|
||||
this.port_explicit = port_explicit;
|
||||
}
|
||||
|
||||
public boolean isMysql_compat_mode() {
|
||||
return mysql_compat_mode;
|
||||
}
|
||||
|
|
@ -203,6 +212,7 @@ public final class ConnectParams {
|
|||
&& Objects.equals(password, that.password)
|
||||
&& Objects.equals(url_params, that.url_params)
|
||||
&& Objects.equals(connection_string, that.connection_string)
|
||||
&& port_explicit == that.port_explicit
|
||||
&& mysql_compat_mode == that.mysql_compat_mode
|
||||
&& Objects.equals(jdbc_driver_class, that.jdbc_driver_class)
|
||||
&& Objects.equals(jdbc_driver_paths, that.jdbc_driver_paths)
|
||||
|
|
@ -217,7 +227,7 @@ public final class ConnectParams {
|
|||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(host, port, database, username, password, url_params, connection_string,
|
||||
mysql_compat_mode, jdbc_driver_class, jdbc_driver_paths, ssl, ca_cert_path, client_cert_path, client_key_path, gbase_server, informix_server);
|
||||
port_explicit, mysql_compat_mode, jdbc_driver_class, jdbc_driver_paths, ssl, ca_cert_path, client_cert_path, client_key_path, gbase_server, informix_server);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -229,6 +239,7 @@ public final class ConnectParams {
|
|||
+ ", password=" + password
|
||||
+ ", url_params=" + url_params
|
||||
+ ", connection_string=" + connection_string
|
||||
+ ", port_explicit=" + port_explicit
|
||||
+ ", mysql_compat_mode=" + mysql_compat_mode
|
||||
+ ", jdbc_driver_class=" + jdbc_driver_class
|
||||
+ ", jdbc_driver_paths=" + jdbc_driver_paths
|
||||
|
|
|
|||
|
|
@ -246,9 +246,10 @@ public final class SqlServerLegacyAgent extends ConfiguredJdbcAgent {
|
|||
}
|
||||
|
||||
String host = normalizedSqlServerHost(params.getHost());
|
||||
boolean usesNamedInstance = usesNamedInstance(host, params.getPort(), params.isPort_explicit());
|
||||
StringBuilder url = new StringBuilder("jdbc:sqlserver://")
|
||||
.append(host);
|
||||
if (!usesNamedInstance(host)) {
|
||||
.append(usesNamedInstance ? host : serverHost(host));
|
||||
if (!usesNamedInstance) {
|
||||
int port = params.getPort() > 0 ? params.getPort() : PROFILE.getDefaultPort();
|
||||
url.append(":").append(port);
|
||||
}
|
||||
|
|
@ -273,9 +274,17 @@ public final class SqlServerLegacyAgent extends ConfiguredJdbcAgent {
|
|||
return server + "\\" + instance;
|
||||
}
|
||||
|
||||
private static boolean usesNamedInstance(String host) {
|
||||
private static boolean usesNamedInstance(String host, int port, boolean portExplicit) {
|
||||
int separator = host.indexOf('\\');
|
||||
return separator > 0 && separator < host.length() - 1;
|
||||
return separator > 0 && separator < host.length() - 1 && (port <= 0 || (port == PROFILE.getDefaultPort() && !portExplicit));
|
||||
}
|
||||
|
||||
private static String serverHost(String host) {
|
||||
int separator = host.indexOf('\\');
|
||||
if (separator > 0 && separator < host.length() - 1) {
|
||||
return host.substring(0, separator).trim();
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
private static String sanitizeSqlServerUrl(String value) {
|
||||
|
|
|
|||
|
|
@ -84,6 +84,45 @@ class SqlServerLegacyAgentTest {
|
|||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyTlsUrlUsesExplicitPortInsteadOfNamedInstanceResolution() {
|
||||
ConnectParams params = new ConnectParams(
|
||||
"db.example.com\\SQLEXPRESS",
|
||||
40030,
|
||||
"appdb",
|
||||
"sa",
|
||||
"secret",
|
||||
"applicationName=dbx",
|
||||
"",
|
||||
false
|
||||
);
|
||||
|
||||
Assertions.assertEquals(
|
||||
"jdbc:sqlserver://db.example.com:40030;databaseName=appdb;applicationName=dbx;encrypt=true;trustServerCertificate=true;sslProtocol=TLSv1",
|
||||
SqlServerLegacyAgent.legacyTlsUrl(params)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyTlsUrlUsesExplicitDefaultPortInsteadOfNamedInstanceResolution() {
|
||||
ConnectParams params = new ConnectParams(
|
||||
"db.example.com\\SQLEXPRESS",
|
||||
1433,
|
||||
"appdb",
|
||||
"sa",
|
||||
"secret",
|
||||
"applicationName=dbx",
|
||||
"",
|
||||
false
|
||||
);
|
||||
params.setPort_explicit(true);
|
||||
|
||||
Assertions.assertEquals(
|
||||
"jdbc:sqlserver://db.example.com:1433;databaseName=appdb;applicationName=dbx;encrypt=true;trustServerCertificate=true;sslProtocol=TLSv1",
|
||||
SqlServerLegacyAgent.legacyTlsUrl(params)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyTlsUrlNormalizesExplicitConnectionString() {
|
||||
ConnectParams params = new ConnectParams(
|
||||
|
|
|
|||
|
|
@ -370,6 +370,33 @@ function sshLayersForConfig(config: LegacyConnectionConfig): SshTunnelConfig[] {
|
|||
}
|
||||
|
||||
const form = ref(defaultForm());
|
||||
|
||||
function externalConfigRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? { ...(value as Record<string, unknown>) } : {};
|
||||
}
|
||||
|
||||
function sqlServerPortExplicitFromConfig(config: Pick<ConnectionConfig, "db_type" | "external_config">): boolean {
|
||||
if (config.db_type !== "sqlserver") return false;
|
||||
const external = externalConfigRecord(config.external_config);
|
||||
return external.portExplicit === true || external.port_explicit === true;
|
||||
}
|
||||
|
||||
function setSqlServerPortExplicit(config: Pick<ConnectionConfig, "db_type"> & { external_config?: unknown }, explicit: boolean) {
|
||||
if (config.db_type !== "sqlserver") return;
|
||||
const next = externalConfigRecord(config.external_config);
|
||||
delete next.port_explicit;
|
||||
if (explicit) {
|
||||
next.portExplicit = true;
|
||||
} else {
|
||||
delete next.portExplicit;
|
||||
}
|
||||
config.external_config = Object.keys(next).length > 0 ? next : undefined;
|
||||
}
|
||||
|
||||
function markSqlServerPortExplicit() {
|
||||
setSqlServerPortExplicit(form.value, true);
|
||||
}
|
||||
|
||||
const keepaliveEnabled = computed({
|
||||
get: () => Number(form.value.keepalive_interval_secs) > 0,
|
||||
set: (enabled: boolean) => {
|
||||
|
|
@ -1319,9 +1346,13 @@ function applyProfile(val: string, preserveConnectionFields = false) {
|
|||
form.value.db_type = profile.type;
|
||||
form.value.driver_profile = val;
|
||||
form.value.driver_label = isCustomCompatibleProfile() ? customDriverName.value.trim() || profile.label : profile.label;
|
||||
if (profile.type !== "sqlserver") {
|
||||
form.value.external_config = undefined;
|
||||
}
|
||||
|
||||
if (!preserveConnectionFields) {
|
||||
form.value.port = profile.port;
|
||||
setSqlServerPortExplicit(form.value, false);
|
||||
form.value.username = profile.user;
|
||||
form.value.url_params = profile.urlParams || "";
|
||||
form.value.agent_java_options = [];
|
||||
|
|
@ -2394,6 +2425,8 @@ function connectionConfigForSubmit(id: string): ConnectionConfig {
|
|||
config.password = config.password.trim();
|
||||
config.database = config.database?.trim() || undefined;
|
||||
}
|
||||
} else if (config.db_type === "sqlserver") {
|
||||
config.external_config = sqlServerPortExplicitFromConfig(config) ? { portExplicit: true } : undefined;
|
||||
} else {
|
||||
config.external_config = undefined;
|
||||
}
|
||||
|
|
@ -3112,7 +3145,7 @@ function resetForm() {
|
|||
const submittedOneTimePrefillKey = ref<string | null>(null);
|
||||
|
||||
function oneTimePrefillKey(draft: ConnectionDeepLinkDraft) {
|
||||
return JSON.stringify([draft.name, draft.dbType, draft.driverProfile, draft.driverLabel, draft.host, draft.port, draft.username, draft.password, draft.database, draft.urlParams, draft.ssl, draft.connectionString, draft.oracleConnectionType, draft.useMongoUrl]);
|
||||
return JSON.stringify([draft.name, draft.dbType, draft.driverProfile, draft.driverLabel, draft.host, draft.port, draft.portExplicit, draft.username, draft.password, draft.database, draft.urlParams, draft.ssl, draft.connectionString, draft.oracleConnectionType, draft.useMongoUrl]);
|
||||
}
|
||||
|
||||
function submitOneTimePrefill(draft: ConnectionDeepLinkDraft) {
|
||||
|
|
@ -3124,7 +3157,7 @@ function submitOneTimePrefill(draft: ConnectionDeepLinkDraft) {
|
|||
}
|
||||
|
||||
function applyConnectionDraftToConfig(config: Omit<ConnectionConfig, "id">, draft: ConnectionDeepLinkDraft): Omit<ConnectionConfig, "id"> {
|
||||
return {
|
||||
const next = {
|
||||
...config,
|
||||
db_type: draft.dbType,
|
||||
driver_profile: draft.driverProfile,
|
||||
|
|
@ -3140,6 +3173,8 @@ function applyConnectionDraftToConfig(config: Omit<ConnectionConfig, "id">, draf
|
|||
oracle_connection_type: draft.oracleConnectionType ?? config.oracle_connection_type,
|
||||
one_time: draft.oneTime || undefined,
|
||||
};
|
||||
setSqlServerPortExplicit(next, draft.portExplicit === true);
|
||||
return next;
|
||||
}
|
||||
|
||||
function applyConnectionDraftToForm(draft: ConnectionDeepLinkDraft) {
|
||||
|
|
@ -4807,7 +4842,7 @@ function openExternalUrl(url: string) {
|
|||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.host") }}</Label>
|
||||
<Input v-model="form.host" class="col-span-2" />
|
||||
<Input v-model.number="form.port" type="number" class="col-span-1" />
|
||||
<Input v-model.number="form.port" type="number" class="col-span-1" @input="markSqlServerPortExplicit" />
|
||||
</div>
|
||||
|
||||
<div v-if="form.driver_profile === 'gbase8s'" class="grid grid-cols-4 items-center gap-4">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export interface ConnectionDeepLinkDraft {
|
|||
driverLabel: string;
|
||||
host?: string;
|
||||
port?: number;
|
||||
portExplicit?: boolean;
|
||||
username?: string;
|
||||
password?: string;
|
||||
database?: string;
|
||||
|
|
@ -54,6 +55,7 @@ function draftFromConnectionUrl(value: string, preferredProfile?: string): Conne
|
|||
driverLabel: parsed.driverLabel,
|
||||
host: parsed.host,
|
||||
port: parsed.port,
|
||||
portExplicit: parsed.portExplicit,
|
||||
username: parsed.username,
|
||||
password: parsed.password,
|
||||
database: parsed.database,
|
||||
|
|
@ -94,12 +96,14 @@ export function parseConnectionDeepLink(value: string): ConnectionDeepLinkDraft
|
|||
})();
|
||||
|
||||
const oneTime = optionalBooleanParam(params, "one_time");
|
||||
const explicitPort = optionalNumberParam(params, "port");
|
||||
|
||||
return {
|
||||
...draft,
|
||||
name: optionalParam(params, "name") ?? draft.name,
|
||||
host: optionalParam(params, "host") ?? draft.host,
|
||||
port: optionalNumberParam(params, "port") ?? draft.port,
|
||||
port: explicitPort ?? draft.port,
|
||||
...((explicitPort !== undefined && draft.dbType === "sqlserver") || draft.portExplicit ? { portExplicit: true } : {}),
|
||||
username: optionalParam(params, "user") ?? draft.username,
|
||||
password: optionalParam(params, "password") ?? draft.password,
|
||||
database: optionalParam(params, "database") ?? draft.database,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export interface ParsedConnectionUrl {
|
|||
connectionString?: string;
|
||||
oracleConnectionType?: "service_name" | "sid";
|
||||
useMongoUrl?: boolean;
|
||||
portExplicit?: boolean;
|
||||
}
|
||||
|
||||
export type ConnectionProfile = {
|
||||
|
|
@ -276,6 +277,7 @@ function parseJdbcSqlServerUrl(source: string): ParsedConnectionUrl | null {
|
|||
driverLabel: profile.label,
|
||||
host: match[1],
|
||||
port: match[2] ? Number(match[2]) : profile.defaultPort,
|
||||
...(match[2] ? { portExplicit: true } : {}),
|
||||
username: decodeUrlPart(props.get("user") || ""),
|
||||
password: decodeUrlPart(props.get("password") || ""),
|
||||
database: decodeUrlPart(props.get("databasename") || props.get("database") || "") || undefined,
|
||||
|
|
@ -568,6 +570,7 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
|
|||
driverLabel: profile.label,
|
||||
host: parsed.hostname,
|
||||
port: parsed.port ? Number(parsed.port) : profile.defaultPort,
|
||||
...(profile.type === "sqlserver" && parsed.port ? { portExplicit: true } : {}),
|
||||
username: mysqlCredentials?.username ?? decodeUrlPart(parsed.username),
|
||||
password: mysqlCredentials?.password ?? decodeUrlPart(parsed.password),
|
||||
database: databaseFromPath(parsed.pathname),
|
||||
|
|
@ -598,6 +601,23 @@ function applyParsedPassword(config: Omit<ConnectionConfig, "id">, parsed: Parse
|
|||
return parsed.password;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function sqlServerExternalConfig(existing: unknown, parsed: ParsedConnectionUrl): unknown {
|
||||
if (parsed.dbType !== "sqlserver") return existing;
|
||||
|
||||
const next = isRecord(existing) ? { ...existing } : {};
|
||||
delete next.port_explicit;
|
||||
if (parsed.portExplicit) {
|
||||
next.portExplicit = true;
|
||||
} else {
|
||||
delete next.portExplicit;
|
||||
}
|
||||
return Object.keys(next).length > 0 ? next : undefined;
|
||||
}
|
||||
|
||||
export function applyParsedConnectionUrl(config: Omit<ConnectionConfig, "id">, parsed: ParsedConnectionUrl): Omit<ConnectionConfig, "id"> {
|
||||
return {
|
||||
...config,
|
||||
|
|
@ -614,5 +634,6 @@ export function applyParsedConnectionUrl(config: Omit<ConnectionConfig, "id">, p
|
|||
ssl: parsed.ssl,
|
||||
connection_string: parsed.connectionString,
|
||||
oracle_connection_type: parsed.oracleConnectionType,
|
||||
external_config: sqlServerExternalConfig(config.external_config, parsed),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ pub fn agent_connect_params(config: &ConnectionConfig, host: &str, port: u16, da
|
|||
serde_json::json!({
|
||||
"host": agent_host,
|
||||
"port": agent_port,
|
||||
"port_explicit": config.sqlserver_port_explicit(),
|
||||
"database": agent_database,
|
||||
"username": config.username,
|
||||
"password": config.password,
|
||||
|
|
|
|||
|
|
@ -685,13 +685,13 @@ impl AppState {
|
|||
port: u16,
|
||||
connect_timeout: Duration,
|
||||
) -> Result<String, String> {
|
||||
match db::sqlserver::connect(
|
||||
match db::sqlserver::connect_with_port_explicit(
|
||||
host,
|
||||
port,
|
||||
config.sqlserver_port_explicit(),
|
||||
&config.username,
|
||||
&config.password,
|
||||
config.database.as_deref(),
|
||||
config.url_params.as_deref(),
|
||||
connect_timeout,
|
||||
)
|
||||
.await
|
||||
|
|
@ -730,13 +730,13 @@ impl AppState {
|
|||
port: u16,
|
||||
connect_timeout: Duration,
|
||||
) -> Result<PoolKind, String> {
|
||||
match db::sqlserver::connect(
|
||||
match db::sqlserver::connect_with_port_explicit(
|
||||
host,
|
||||
port,
|
||||
config.sqlserver_port_explicit(),
|
||||
&config.username,
|
||||
&config.password,
|
||||
config.database.as_deref(),
|
||||
config.url_params.as_deref(),
|
||||
connect_timeout,
|
||||
)
|
||||
.await
|
||||
|
|
@ -3482,6 +3482,17 @@ mod tests {
|
|||
assert_eq!(params["url_params"], "INFORMIXSERVER=informix;CLIENT_LOCALE=en_US.utf8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_connect_params_include_sqlserver_explicit_port_state() {
|
||||
let mut config = mysql_config(Some("master"));
|
||||
config.db_type = DatabaseType::SqlServer;
|
||||
config.external_config = Some(serde_json::json!({ "portExplicit": true }));
|
||||
|
||||
let params = agent_connect_params(&config, r"db.example.com\SQLEXPRESS", 1433, "master");
|
||||
|
||||
assert_eq!(params["port_explicit"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn databend_uses_agent_pool_not_bare_mysql_pool() {
|
||||
assert!(uses_bare_mysql_pool(&DatabaseType::Doris));
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ pub type SqlServerClient = Client<Compat<TcpStream>>;
|
|||
pub const SQLSERVER_DRIVER_PANIC_ERROR_PREFIX: &str = "SQL Server driver panic:";
|
||||
pub const SQLSERVER_LEGACY_DRIVER_PROFILE: &str = "sqlserver-legacy";
|
||||
pub const SQLSERVER_LEGACY_DRIVER_LABEL: &str = "SQL Server legacy compatibility component";
|
||||
const SQLSERVER_DEFAULT_PORT: u16 = 1433;
|
||||
const SIMPLE_QUERY_MODULE_KEYWORDS: &[&str] = &["FUNCTION", "PROC", "PROCEDURE", "TRIGGER", "VIEW"];
|
||||
// Match JDBC/tiberius `encrypt=false`: encrypt only login, then drop back to raw TDS.
|
||||
const SQLSERVER_LEGACY_ENCRYPTION_LEVEL: tiberius::EncryptionLevel = tiberius::EncryptionLevel::Off;
|
||||
|
|
@ -45,6 +46,10 @@ fn sqlserver_endpoint(host: &str) -> SqlServerEndpoint<'_> {
|
|||
SqlServerEndpoint { host: host.trim(), instance_name: None }
|
||||
}
|
||||
|
||||
fn sqlserver_uses_named_instance_resolution(endpoint: &SqlServerEndpoint<'_>, port: u16, port_explicit: bool) -> bool {
|
||||
endpoint.instance_name.is_some() && (port == 0 || (port == SQLSERVER_DEFAULT_PORT && !port_explicit))
|
||||
}
|
||||
|
||||
fn query_result_row_limit(max_rows: Option<usize>) -> usize {
|
||||
max_rows.unwrap_or(MAX_ROWS).max(1)
|
||||
}
|
||||
|
|
@ -58,13 +63,28 @@ pub async fn connect(
|
|||
_url_params: Option<&str>,
|
||||
timeout: Duration,
|
||||
) -> Result<SqlServerClient, String> {
|
||||
match try_connect(host, port, user, pass, database, tiberius::EncryptionLevel::Required, timeout).await {
|
||||
connect_with_port_explicit(host, port, false, user, pass, database, timeout).await
|
||||
}
|
||||
|
||||
pub async fn connect_with_port_explicit(
|
||||
host: &str,
|
||||
port: u16,
|
||||
port_explicit: bool,
|
||||
user: &str,
|
||||
pass: &str,
|
||||
database: Option<&str>,
|
||||
timeout: Duration,
|
||||
) -> Result<SqlServerClient, String> {
|
||||
match try_connect(host, port, port_explicit, user, pass, database, tiberius::EncryptionLevel::Required, timeout)
|
||||
.await
|
||||
{
|
||||
Ok(client) => Ok(client),
|
||||
Err(encrypted_error) => try_connect_legacy_sqlserver_encryption(host, port, user, pass, database, timeout)
|
||||
.await
|
||||
.map_err(|plain_error| {
|
||||
if is_sqlserver_tls_handshake_error(&encrypted_error) {
|
||||
format!(
|
||||
Err(encrypted_error) => {
|
||||
try_connect_legacy_sqlserver_encryption(host, port, port_explicit, user, pass, database, timeout)
|
||||
.await
|
||||
.map_err(|plain_error| {
|
||||
if is_sqlserver_tls_handshake_error(&encrypted_error) {
|
||||
format!(
|
||||
"{encrypted_error}\n\nThis may be caused by an old SQL Server TLS/encryption configuration. \
|
||||
If you are connecting to SQL Server 2008/2008 R2/2012 or another legacy instance, \
|
||||
try SQL Server legacy compatibility mode. It first behaves like encrypt=false and, \
|
||||
|
|
@ -73,16 +93,18 @@ pub async fn connect(
|
|||
or SSH tunnels.\n\n\
|
||||
Automatic native legacy fallback also failed: {plain_error}"
|
||||
)
|
||||
} else {
|
||||
plain_error
|
||||
}
|
||||
}),
|
||||
} else {
|
||||
plain_error
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_connect_legacy_sqlserver_encryption(
|
||||
host: &str,
|
||||
port: u16,
|
||||
port_explicit: bool,
|
||||
user: &str,
|
||||
pass: &str,
|
||||
database: Option<&str>,
|
||||
|
|
@ -90,7 +112,7 @@ async fn try_connect_legacy_sqlserver_encryption(
|
|||
) -> Result<SqlServerClient, String> {
|
||||
let mut errors = Vec::new();
|
||||
for (label, encryption) in SQLSERVER_LEGACY_ENCRYPTION_FALLBACKS {
|
||||
match try_connect(host, port, user, pass, database, encryption, timeout).await {
|
||||
match try_connect(host, port, port_explicit, user, pass, database, encryption, timeout).await {
|
||||
Ok(client) => return Ok(client),
|
||||
Err(error) => errors.push(format!("{label} failed: {error}")),
|
||||
}
|
||||
|
|
@ -120,6 +142,7 @@ fn is_sqlserver_tls_handshake_error(error: &str) -> bool {
|
|||
async fn try_connect(
|
||||
host: &str,
|
||||
port: u16,
|
||||
port_explicit: bool,
|
||||
user: &str,
|
||||
pass: &str,
|
||||
database: Option<&str>,
|
||||
|
|
@ -128,8 +151,9 @@ async fn try_connect(
|
|||
) -> Result<SqlServerClient, String> {
|
||||
let mut config = Config::new();
|
||||
let endpoint = sqlserver_endpoint(host);
|
||||
let uses_named_instance_resolution = sqlserver_uses_named_instance_resolution(&endpoint, port, port_explicit);
|
||||
config.host(endpoint.host);
|
||||
if let Some(instance_name) = endpoint.instance_name {
|
||||
if let Some(instance_name) = endpoint.instance_name.filter(|_| uses_named_instance_resolution) {
|
||||
config.instance_name(instance_name);
|
||||
} else {
|
||||
config.port(port);
|
||||
|
|
@ -141,7 +165,7 @@ async fn try_connect(
|
|||
config.trust_cert();
|
||||
config.encryption(encryption);
|
||||
|
||||
let tcp = if endpoint.instance_name.is_some() {
|
||||
let tcp = if uses_named_instance_resolution {
|
||||
tokio::time::timeout(timeout, TcpStream::connect_named(&config))
|
||||
.await
|
||||
.map_err(|_| format!("SQL Server connection timed out ({}s)", timeout.as_secs()))?
|
||||
|
|
@ -1893,6 +1917,15 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_named_instance_resolution_yields_to_explicit_port() {
|
||||
let endpoint = super::sqlserver_endpoint(r"db.example.com\SQLEXPRESS");
|
||||
assert!(super::sqlserver_uses_named_instance_resolution(&endpoint, 0, false));
|
||||
assert!(super::sqlserver_uses_named_instance_resolution(&endpoint, 1433, false));
|
||||
assert!(!super::sqlserver_uses_named_instance_resolution(&endpoint, 1433, true));
|
||||
assert!(!super::sqlserver_uses_named_instance_resolution(&endpoint, 40030, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_connect_uses_named_instance_resolution() {
|
||||
let source = include_str!("sqlserver.rs");
|
||||
|
|
|
|||
|
|
@ -1205,6 +1205,17 @@ impl ConnectionConfig {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn sqlserver_port_explicit(&self) -> bool {
|
||||
if self.db_type != DatabaseType::SqlServer {
|
||||
return false;
|
||||
}
|
||||
self.external_config
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("portExplicit").or_else(|| value.get("port_explicit")))
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn message_queue_admin_url(&self) -> String {
|
||||
self.external_config
|
||||
.as_ref()
|
||||
|
|
|
|||
|
|
@ -33,6 +33,23 @@ test("parses encoded database URL with password", () => {
|
|||
assert.equal(draft?.urlParams, "sslmode=require");
|
||||
});
|
||||
|
||||
test("preserves explicit SQL Server default port from nested URLs", () => {
|
||||
const nested = encodeURIComponent("sqlserver://sa:secret@db.internal:1433/erp");
|
||||
const draft = parseConnectionDeepLink(`dbx://connection/new?url=${nested}`);
|
||||
|
||||
assert.equal(draft?.dbType, "sqlserver");
|
||||
assert.equal(draft?.port, 1433);
|
||||
assert.equal(draft?.portExplicit, true);
|
||||
});
|
||||
|
||||
test("marks top-level SQL Server ports explicit for one-time links", () => {
|
||||
const draft = parseConnectionDeepLink("dbx://connection/new?type=sqlserver&host=db\\instance&port=1433&one_time=1");
|
||||
|
||||
assert.equal(draft?.port, 1433);
|
||||
assert.equal(draft?.portExplicit, true);
|
||||
assert.equal(draft?.oneTime, true);
|
||||
});
|
||||
|
||||
test("uses the nested database URL name as connection name", () => {
|
||||
const nested = encodeURIComponent("mysql://root:123456@localhost/?name=%E5%85%AC%E5%8F%B8+-+%E6%9C%AC%E5%9C%B0Docker&charset=utf8mb4");
|
||||
const draft = parseConnectionDeepLink(`dbx://connection/new?url=${nested}`);
|
||||
|
|
|
|||
|
|
@ -274,6 +274,25 @@ test("parses SQL Server JDBC URLs with semicolon properties", () => {
|
|||
assert.equal(parsed.password, "s@cret");
|
||||
assert.equal(parsed.database, "erp");
|
||||
assert.equal(parsed.urlParams, "encrypt=true");
|
||||
assert.equal(parsed.portExplicit, true);
|
||||
});
|
||||
|
||||
test("marks explicit SQL Server default port when applying connection URLs", () => {
|
||||
const parsed = parseConnectionUrl("jdbc:sqlserver://sql.example.com\\SQLEXPRESS:1433;databaseName=erp;user=sa;password=secret");
|
||||
const applied = applyParsedConnectionUrl({ name: "", db_type: "sqlserver", username: "", password: "" } as any, parsed);
|
||||
|
||||
assert.equal(parsed.port, 1433);
|
||||
assert.equal(parsed.portExplicit, true);
|
||||
assert.deepEqual(applied.external_config, { portExplicit: true });
|
||||
});
|
||||
|
||||
test("does not mark SQL Server default port explicit when connection URL omits it", () => {
|
||||
const parsed = parseConnectionUrl("jdbc:sqlserver://sql.example.com\\SQLEXPRESS;databaseName=erp;user=sa;password=secret");
|
||||
const applied = applyParsedConnectionUrl({ name: "", db_type: "sqlserver", username: "", password: "" } as any, parsed);
|
||||
|
||||
assert.equal(parsed.port, 1433);
|
||||
assert.equal(parsed.portExplicit, undefined);
|
||||
assert.equal(applied.external_config, undefined);
|
||||
});
|
||||
|
||||
test("parses H2 split JDBC URLs as file connections", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue