Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 0.6.50
- Added support for SQL Server named instances with automatic port discovery via SSRP (SQL Server Resolution Protocol). You can now connect using `mssql://user:pass@host/db?instance=SQLEXPRESS` and the port will be automatically discovered.

## 0.6.49
- Added support for ODBC. SQLx-oldapi can now connect to Oracle, Db2, Snowflake, BigQuery, Databricks, and many other databases, using locally installed ODBC drivers.

Expand Down
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ unexpected_cfgs = { level = "warn", check-cfg = [

[package]
name = "sqlx-oldapi"
version = "0.6.49"
version = "0.6.50"
license = "MIT OR Apache-2.0"
readme = "README.md"
repository = "https://github.com/lovasoa/sqlx"
Expand Down Expand Up @@ -155,8 +155,8 @@ bstr = ["sqlx-core/bstr"]
git2 = ["sqlx-core/git2"]

[dependencies]
sqlx-core = { package = "sqlx-core-oldapi", version = "0.6.49", path = "sqlx-core", default-features = false }
sqlx-macros = { package = "sqlx-macros-oldapi", version = "0.6.49", path = "sqlx-macros", default-features = false, optional = true }
sqlx-core = { package = "sqlx-core-oldapi", version = "0.6.50", path = "sqlx-core", default-features = false }
sqlx-macros = { package = "sqlx-macros-oldapi", version = "0.6.50", path = "sqlx-macros", default-features = false, optional = true }

[dev-dependencies]
anyhow = "1.0.52"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
> - Multiple bug fixes around string handling, including better support for long strings
> - Support for packet chunking, which fixes a bug where large bound parameters or large queries would fail
> - Support for TLS encrypted connections
> - Support for named instances with automatic port discovery via SSRP
>
> The main use case driving the development of sqlx-oldapi is the [SQLPage](https://sql.datapage.app/) SQL-only rapid application building tool.

Expand Down
2 changes: 1 addition & 1 deletion examples/postgres/axum-social-with-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ publish = false
[dependencies]
# Primary crates
axum = { version = "0.5.13", features = ["macros"] }
sqlx = { package = "sqlx-oldapi", version = "0.6.49", path = "../../../", features = ["runtime-tokio-rustls", "postgres", "time", "uuid"] }
sqlx = { package = "sqlx-oldapi", version = "0.6.50", path = "../../../", features = ["runtime-tokio-rustls", "postgres", "time", "uuid"] }
tokio = { version = "1.20.1", features = ["rt-multi-thread", "macros"] }

# Important secondary crates
Expand Down
4 changes: 2 additions & 2 deletions sqlx-bench/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ sqlite = ["sqlx/sqlite"]
criterion = "0.3.3"
dotenvy = "0.15.0"
once_cell = "1.4"
sqlx = { package = "sqlx-oldapi", version = "0.6.49", path = "../", default-features = false, features = ["macros"] }
sqlx-rt = { package = "sqlx-rt-oldapi", version = "0.6.49", path = "../sqlx-rt", default-features = false }
sqlx = { package = "sqlx-oldapi", version = "0.6.50", path = "../", default-features = false, features = ["macros"] }
sqlx-rt = { package = "sqlx-rt-oldapi", version = "0.6.50", path = "../sqlx-rt", default-features = false }

chrono = "0.4.19"

Expand Down
4 changes: 2 additions & 2 deletions sqlx-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sqlx-cli"
version = "0.6.49"
version = "0.6.50"
description = "Command-line utility for SQLx, the Rust SQL toolkit."
edition = "2021"
readme = "README.md"
Expand Down Expand Up @@ -28,7 +28,7 @@ path = "src/bin/cargo-sqlx.rs"
[dependencies]
dotenvy = "0.15.0"
tokio = { version = "1.15.0", features = ["macros", "rt", "rt-multi-thread"] }
sqlx = { package = "sqlx-oldapi", version = "0.6.49", path = "..", default-features = false, features = [
sqlx = { package = "sqlx-oldapi", version = "0.6.50", path = "..", default-features = false, features = [
"migrate",
"any",
"offline",
Expand Down
4 changes: 2 additions & 2 deletions sqlx-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sqlx-core-oldapi"
version = "0.6.49"
version = "0.6.50"
repository = "https://github.com/lovasoa/sqlx"
description = "Core of SQLx, the rust SQL toolkit. Not intended to be used directly."
license = "MIT OR Apache-2.0"
Expand Down Expand Up @@ -104,7 +104,7 @@ offline = ["serde", "either/serde"]
paste = "1.0.6"
ahash = "0.8.3"
atoi = "2.0.0"
sqlx-rt = { path = "../sqlx-rt", version = "0.6.49", package = "sqlx-rt-oldapi" }
sqlx-rt = { path = "../sqlx-rt", version = "0.6.50", package = "sqlx-rt-oldapi" }
base64 = { version = "0.22", default-features = false, optional = true, features = ["std"] }
bigdecimal_ = { version = "0.4.1", optional = true, package = "bigdecimal" }
rust_decimal = { version = "1.19.0", optional = true }
Expand Down
1 change: 1 addition & 0 deletions sqlx-core/src/mssql/connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use std::sync::Arc;
mod establish;
mod executor;
mod prepare;
mod ssrp;
mod stream;
mod tls_prelogin_stream_wrapper;

Expand Down
229 changes: 229 additions & 0 deletions sqlx-core/src/mssql/connection/ssrp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
use crate::error::Error;
use encoding_rs::WINDOWS_1252;
use sqlx_rt::{timeout, UdpSocket};
use std::time::Duration;

const SSRP_PORT: u16 = 1434;
const CLNT_UCAST_INST: u8 = 0x04;
const SVR_RESP: u8 = 0x05;
const SSRP_TIMEOUT: Duration = Duration::from_secs(1);

struct InstanceInfo<'a> {
server_name: Option<&'a str>,
instance_name: Option<&'a str>,
is_clustered: Option<bool>,
version: Option<&'a str>,
tcp_port: Option<u16>,
}

pub(crate) async fn resolve_instance_port(server: &str, instance: &str) -> Result<u16, Error> {
log::debug!(
"resolving SQL Server instance port for '{}' on server '{}'",
instance,
server
);

let mut request = Vec::with_capacity(1 + instance.len() + 1);
request.push(CLNT_UCAST_INST);
request.extend_from_slice(instance.as_bytes());
request.push(0);

let socket = UdpSocket::bind("0.0.0.0:0")
.await
.map_err(|e| err_protocol!("failed to bind UDP socket for SSRP: {}", e))?;

log::debug!(
"sending SSRP CLNT_UCAST_INST request to {}:{} for instance '{}'",
server,
SSRP_PORT,
instance
);

socket
.send_to(&request, (server, SSRP_PORT))
.await
.map_err(|e| {
err_protocol!(
"failed to send SSRP request to {}:{}: {}",
server,
SSRP_PORT,
e
)
})?;

let mut buffer = [0u8; 1024];
let bytes_read = timeout(SSRP_TIMEOUT, socket.recv(&mut buffer))
.await
.map_err(|_| {
err_protocol!(
"SSRP request to {} for instance {} timed out after {:?}",
server,
instance,
SSRP_TIMEOUT
)
})?
.map_err(|e| {
err_protocol!(
"failed to receive SSRP response from {} for instance {}: {}",
server,
instance,
e
)
})?;

log::debug!(
"received SSRP response from {} ({} bytes)",
server,
bytes_read
);

if bytes_read < 3 {
return Err(err_protocol!(
"SSRP response too short: {} bytes",
bytes_read
));
}

if buffer[0] != SVR_RESP {
return Err(err_protocol!(
"invalid SSRP response type: expected 0x05, got 0x{:02x}",
buffer[0]
));
}

let response_size = u16::from_le_bytes([buffer[1], buffer[2]]) as usize;
if response_size + 3 > bytes_read {
return Err(err_protocol!(
"SSRP response size mismatch: expected {} bytes, got {}",
response_size + 3,
bytes_read
));
}

let response_bytes = &buffer[3..(3 + response_size)];
let (response_str, _encoding_used, had_errors) = WINDOWS_1252.decode(response_bytes);

if had_errors {
log::debug!("SSRP response had MBCS decoding errors, continuing anyway");
}

log::debug!("SSRP response data: {}", response_str);

find_instance_tcp_port(&response_str, instance)
}

fn find_instance_tcp_port(data: &str, instance_name: &str) -> Result<u16, Error> {
for instance_data in data.split(";;") {
if instance_data.is_empty() {
continue;
}

let info = parse_instance_info(instance_data);

if let Some(name) = info.instance_name {
log::debug!("found instance '{}' in SSRP response", name);

if name.eq_ignore_ascii_case(instance_name) {
log::debug!(
"instance '{}' matches requested instance '{}'",
name,
instance_name
);

if let Some(port) = info.tcp_port {
log::debug!("resolved instance '{}' to port {}", instance_name, port);
return Ok(port);
} else {
return Err(err_protocol!(
"instance '{}' found but no TCP port available",
instance_name
));
}
}
}
}

Err(err_protocol!(
"instance '{}' not found in SSRP response",
instance_name
))
}

fn parse_instance_info<'a>(data: &'a str) -> InstanceInfo<'a> {
let mut info = InstanceInfo {
server_name: None,
instance_name: None,
is_clustered: None,
version: None,
tcp_port: None,
};

let mut tokens = data.split(';');
while let Some(key) = tokens.next() {
let value = tokens.next();

match key {
"ServerName" => info.server_name = value,
"InstanceName" => info.instance_name = value,
"IsClustered" => {
info.is_clustered = value.and_then(|v| match v {
"Yes" => Some(true),
"No" => Some(false),
_ => None,
});
}
"Version" => info.version = value,
"tcp" => {
info.tcp_port = value.and_then(|v| v.parse::<u16>().ok());
}
_ => {
if !key.is_empty() {
log::debug!("ignoring unknown SSRP key: '{}'", key);
}
}
}
}

info
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_find_instance_tcp_port_single_instance() {
let data = "ServerName;MYSERVER;InstanceName;SQLEXPRESS;IsClustered;No;Version;15.0.2000.5;tcp;1433;;";
let port = find_instance_tcp_port(data, "SQLEXPRESS").unwrap();
assert_eq!(port, 1433);
}

#[test]
fn test_find_instance_tcp_port_multiple_instances() {
let data = "ServerName;SRV1;InstanceName;INST1;IsClustered;No;Version;15.0.2000.5;tcp;1433;;ServerName;SRV1;InstanceName;INST2;IsClustered;No;Version;16.0.1000.6;tcp;1434;np;\\\\SRV1\\pipe\\MSSQL$INST2\\sql\\query;;";
let port = find_instance_tcp_port(data, "INST2").unwrap();
assert_eq!(port, 1434);
}

#[test]
fn test_find_instance_tcp_port_case_insensitive() {
let data = "ServerName;MYSERVER;InstanceName;SQLExpress;IsClustered;No;Version;15.0.2000.5;tcp;1433;;";
let port = find_instance_tcp_port(data, "sqlexpress").unwrap();
assert_eq!(port, 1433);
}

#[test]
fn test_find_instance_tcp_port_instance_not_found() {
let data = "ServerName;MYSERVER;InstanceName;SQLEXPRESS;IsClustered;No;Version;15.0.2000.5;tcp;1433;;";
let result = find_instance_tcp_port(data, "NOTFOUND");
assert!(result.is_err());
}

#[test]
fn test_find_instance_tcp_port_no_tcp_port() {
let data =
"ServerName;MYSERVER;InstanceName;SQLEXPRESS;IsClustered;No;Version;15.0.2000.5;;";
let result = find_instance_tcp_port(data, "SQLEXPRESS");
assert!(result.is_err());
}
}
Loading
Loading