diff --git a/agents/common/src/main/java/com/dbx/agent/ConnectParams.java b/agents/common/src/main/java/com/dbx/agent/ConnectParams.java index 3e4639a42..1e4c911b8 100644 --- a/agents/common/src/main/java/com/dbx/agent/ConnectParams.java +++ b/agents/common/src/main/java/com/dbx/agent/ConnectParams.java @@ -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 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 diff --git a/agents/drivers/sqlserver-legacy/src/main/java/com/dbx/agent/sqlserverlegacy/SqlServerLegacyAgent.java b/agents/drivers/sqlserver-legacy/src/main/java/com/dbx/agent/sqlserverlegacy/SqlServerLegacyAgent.java index 6aac71271..7ec3e19b5 100644 --- a/agents/drivers/sqlserver-legacy/src/main/java/com/dbx/agent/sqlserverlegacy/SqlServerLegacyAgent.java +++ b/agents/drivers/sqlserver-legacy/src/main/java/com/dbx/agent/sqlserverlegacy/SqlServerLegacyAgent.java @@ -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) { diff --git a/agents/drivers/sqlserver-legacy/src/test/java/com/dbx/agent/sqlserverlegacy/SqlServerLegacyAgentTest.java b/agents/drivers/sqlserver-legacy/src/test/java/com/dbx/agent/sqlserverlegacy/SqlServerLegacyAgentTest.java index a52436776..15d78965d 100644 --- a/agents/drivers/sqlserver-legacy/src/test/java/com/dbx/agent/sqlserverlegacy/SqlServerLegacyAgentTest.java +++ b/agents/drivers/sqlserver-legacy/src/test/java/com/dbx/agent/sqlserverlegacy/SqlServerLegacyAgentTest.java @@ -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( diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index ee8c297cd..595670dbe 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -370,6 +370,33 @@ function sshLayersForConfig(config: LegacyConnectionConfig): SshTunnelConfig[] { } const form = ref(defaultForm()); + +function externalConfigRecord(value: unknown): Record { + return value && typeof value === "object" && !Array.isArray(value) ? { ...(value as Record) } : {}; +} + +function sqlServerPortExplicitFromConfig(config: Pick): 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 & { 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(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, draft: ConnectionDeepLinkDraft): Omit { - return { + const next = { ...config, db_type: draft.dbType, driver_profile: draft.driverProfile, @@ -3140,6 +3173,8 @@ function applyConnectionDraftToConfig(config: Omit, 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) {
- +
diff --git a/apps/desktop/src/lib/connection/connectionDeepLink.ts b/apps/desktop/src/lib/connection/connectionDeepLink.ts index 4c28c611b..668ad8a19 100644 --- a/apps/desktop/src/lib/connection/connectionDeepLink.ts +++ b/apps/desktop/src/lib/connection/connectionDeepLink.ts @@ -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, diff --git a/apps/desktop/src/lib/connection/connectionUrl.ts b/apps/desktop/src/lib/connection/connectionUrl.ts index f8d0a56d4..c9af4b472 100644 --- a/apps/desktop/src/lib/connection/connectionUrl.ts +++ b/apps/desktop/src/lib/connection/connectionUrl.ts @@ -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, parsed: Parse return parsed.password; } +function isRecord(value: unknown): value is Record { + 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, parsed: ParsedConnectionUrl): Omit { return { ...config, @@ -614,5 +634,6 @@ export function applyParsedConnectionUrl(config: Omit, p ssl: parsed.ssl, connection_string: parsed.connectionString, oracle_connection_type: parsed.oracleConnectionType, + external_config: sqlServerExternalConfig(config.external_config, parsed), }; } diff --git a/crates/dbx-core/src/agent_connection.rs b/crates/dbx-core/src/agent_connection.rs index 632e97cca..d567df80f 100644 --- a/crates/dbx-core/src/agent_connection.rs +++ b/crates/dbx-core/src/agent_connection.rs @@ -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, diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index b3c0ef3fd..54b404794 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -685,13 +685,13 @@ impl AppState { port: u16, connect_timeout: Duration, ) -> Result { - 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 { - 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)); diff --git a/crates/dbx-core/src/db/sqlserver.rs b/crates/dbx-core/src/db/sqlserver.rs index 6fa3738e6..d5157e5d3 100644 --- a/crates/dbx-core/src/db/sqlserver.rs +++ b/crates/dbx-core/src/db/sqlserver.rs @@ -18,6 +18,7 @@ pub type SqlServerClient = Client>; 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 { max_rows.unwrap_or(MAX_ROWS).max(1) } @@ -58,13 +63,28 @@ pub async fn connect( _url_params: Option<&str>, timeout: Duration, ) -> Result { - 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 { + 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 { 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 { 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"); diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index 5b881518c..19e020d5e 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.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() diff --git a/packages/app-tests/connectionDeepLink.test.ts b/packages/app-tests/connectionDeepLink.test.ts index 45cad4131..27237f651 100644 --- a/packages/app-tests/connectionDeepLink.test.ts +++ b/packages/app-tests/connectionDeepLink.test.ts @@ -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}`); diff --git a/packages/app-tests/connectionUrl.test.ts b/packages/app-tests/connectionUrl.test.ts index 1db0d9f8e..339107b58 100644 --- a/packages/app-tests/connectionUrl.test.ts +++ b/packages/app-tests/connectionUrl.test.ts @@ -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", () => {