Skip to content

Commit 597db7d

Browse files
committed
odbc: DSN parsing (odbc:Name -> DSN=Name); chrono/uuid decoding robustness (trim, accept Other/Unknown); add ODBC unit tests for padded values; combine CI ODBC job (Postgres + SQLite); run fmt
1 parent 8b90d35 commit 597db7d

File tree

5 files changed

+54
-42
lines changed

5 files changed

+54
-42
lines changed

.github/workflows/sqlx.yml

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -339,37 +339,8 @@ jobs:
339339
env:
340340
DATABASE_URL: mssql://sa:Password123!@localhost/sqlx
341341
342-
odbc-sqlite:
343-
name: ODBC (SQLite)
344-
runs-on: ubuntu-22.04
345-
needs: check
346-
steps:
347-
- uses: actions/checkout@v4
348-
- uses: dtolnay/rust-toolchain@stable
349-
- uses: Swatinem/rust-cache@v2
350-
with:
351-
prefix-key: v1-sqlx
352-
shared-key: odbc-sqlite
353-
save-if: ${{ false }}
354-
- name: Install ODBC drivers (SQLite)
355-
run: |
356-
sudo apt-get update
357-
sudo apt-get install -y unixodbc odbcinst libsqliteodbc
358-
odbcinst -q -d || true
359-
- name: Build with ODBC feature
360-
run: |
361-
cargo build --manifest-path sqlx-core/Cargo.toml \
362-
--no-default-features \
363-
--features odbc,runtime-tokio-rustls
364-
- name: Run ODBC SQLite tests
365-
run: |
366-
cargo test \
367-
--no-default-features \
368-
--features odbc,runtime-tokio-rustls \
369-
--test odbc-sqlite
370-
371342
odbc:
372-
name: ODBC (PostgreSQL via unixODBC)
343+
name: ODBC (PostgreSQL and SQLite)
373344
runs-on: ubuntu-22.04
374345
needs: check
375346
steps:
@@ -384,10 +355,10 @@ jobs:
384355
run: |
385356
docker compose -f tests/docker-compose.yml run -d -p 5432:5432 --name postgres_16_no_ssl postgres_16_no_ssl
386357
docker exec postgres_16_no_ssl bash -c "until pg_isready; do sleep 1; done"
387-
- name: Install unixODBC and PostgreSQL ODBC driver
358+
- name: Install unixODBC and ODBC drivers (PostgreSQL, SQLite)
388359
run: |
389360
sudo apt-get update
390-
sudo apt-get install -y unixodbc odbcinst odbcinst1debian2 odbc-postgresql
361+
sudo apt-get install -y unixodbc odbcinst odbcinst1debian2 odbc-postgresql libsqliteodbc
391362
odbcinst -j
392363
- name: Configure system/user DSN for PostgreSQL
393364
run: |
@@ -405,3 +376,8 @@ jobs:
405376
cargo test --no-default-features --features any,odbc,macros,all-types,runtime-tokio-rustls -- --test odbc
406377
env:
407378
DATABASE_URL: DSN=SQLX_PG_5432;UID=postgres;PWD=password
379+
- name: Run ODBC tests (SQLite driver)
380+
run: |
381+
cargo test --no-default-features --features any,odbc,macros,all-types,runtime-tokio-rustls -- --test odbc
382+
env:
383+
DATABASE_URL: Driver={SQLite3 ODBC Driver};Database=./tests/odbc/sqlite.db

sqlx-core/src/odbc/options/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ impl FromStr for OdbcConnectOptions {
4848
format!("DSN={}", t)
4949
};
5050

51-
Ok(Self { conn_str, log_settings: LogSettings::default() })
51+
Ok(Self {
52+
conn_str,
53+
log_settings: LogSettings::default(),
54+
})
5255
}
5356
}
5457

sqlx-core/src/odbc/types/chrono.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ impl Type<Odbc> for NaiveDate {
1111
OdbcTypeInfo::DATE
1212
}
1313
fn compatible(ty: &OdbcTypeInfo) -> bool {
14-
matches!(ty.data_type(), DataType::Date) || ty.data_type().accepts_character_data()
14+
matches!(ty.data_type(), DataType::Date)
15+
|| ty.data_type().accepts_character_data()
16+
|| matches!(ty.data_type(), DataType::Other { .. } | DataType::Unknown)
1517
}
1618
}
1719

@@ -20,7 +22,9 @@ impl Type<Odbc> for NaiveTime {
2022
OdbcTypeInfo::TIME
2123
}
2224
fn compatible(ty: &OdbcTypeInfo) -> bool {
23-
matches!(ty.data_type(), DataType::Time { .. }) || ty.data_type().accepts_character_data()
25+
matches!(ty.data_type(), DataType::Time { .. })
26+
|| ty.data_type().accepts_character_data()
27+
|| matches!(ty.data_type(), DataType::Other { .. } | DataType::Unknown)
2428
}
2529
}
2630

@@ -31,6 +35,7 @@ impl Type<Odbc> for NaiveDateTime {
3135
fn compatible(ty: &OdbcTypeInfo) -> bool {
3236
matches!(ty.data_type(), DataType::Timestamp { .. })
3337
|| ty.data_type().accepts_character_data()
38+
|| matches!(ty.data_type(), DataType::Other { .. } | DataType::Unknown)
3439
}
3540
}
3641

@@ -41,6 +46,7 @@ impl Type<Odbc> for DateTime<Utc> {
4146
fn compatible(ty: &OdbcTypeInfo) -> bool {
4247
matches!(ty.data_type(), DataType::Timestamp { .. })
4348
|| ty.data_type().accepts_character_data()
49+
|| matches!(ty.data_type(), DataType::Other { .. } | DataType::Unknown)
4450
}
4551
}
4652

@@ -51,6 +57,7 @@ impl Type<Odbc> for DateTime<FixedOffset> {
5157
fn compatible(ty: &OdbcTypeInfo) -> bool {
5258
matches!(ty.data_type(), DataType::Timestamp { .. })
5359
|| ty.data_type().accepts_character_data()
60+
|| matches!(ty.data_type(), DataType::Other { .. } | DataType::Unknown)
5461
}
5562
}
5663

@@ -61,6 +68,7 @@ impl Type<Odbc> for DateTime<Local> {
6168
fn compatible(ty: &OdbcTypeInfo) -> bool {
6269
matches!(ty.data_type(), DataType::Timestamp { .. })
6370
|| ty.data_type().accepts_character_data()
71+
|| matches!(ty.data_type(), DataType::Other { .. } | DataType::Unknown)
6472
}
6573
}
6674

@@ -168,8 +176,18 @@ impl<'r> Decode<'r, Odbc> for NaiveTime {
168176

169177
impl<'r> Decode<'r, Odbc> for NaiveDateTime {
170178
fn decode(value: OdbcValueRef<'r>) -> Result<Self, BoxDynError> {
171-
let s = <String as Decode<'r, Odbc>>::decode(value)?;
172-
Ok(s.parse()?)
179+
let mut s = <String as Decode<'r, Odbc>>::decode(value)?;
180+
// Some ODBC drivers (e.g. PostgreSQL) may include trailing spaces or NULs
181+
// in textual representations of timestamps. Trim them before parsing.
182+
if s.ends_with('\u{0}') {
183+
s = s.trim_end_matches('\u{0}').to_string();
184+
}
185+
let s_trimmed = s.trim();
186+
// Try strict format first, then fall back to Chrono's FromStr
187+
if let Ok(dt) = NaiveDateTime::parse_from_str(s_trimmed, "%Y-%m-%d %H:%M:%S") {
188+
return Ok(dt);
189+
}
190+
Ok(s_trimmed.parse()?)
173191
}
174192
}
175193

sqlx-core/src/odbc/types/uuid.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ impl Type<Odbc> for Uuid {
1212
// UUID string length
1313
}
1414
fn compatible(ty: &OdbcTypeInfo) -> bool {
15-
ty.data_type().accepts_character_data() || ty.data_type().accepts_binary_data()
15+
ty.data_type().accepts_character_data()
16+
|| ty.data_type().accepts_binary_data()
17+
|| matches!(
18+
ty.data_type(),
19+
odbc_api::DataType::Other { .. } | odbc_api::DataType::Unknown
20+
)
1621
}
1722
}
1823

@@ -32,14 +37,13 @@ impl<'r> Decode<'r, Odbc> for Uuid {
3237
fn decode(value: OdbcValueRef<'r>) -> Result<Self, BoxDynError> {
3338
if let Some(bytes) = value.blob {
3439
if bytes.len() == 16 {
35-
// Binary UUID format
3640
return Ok(Uuid::from_bytes(bytes.try_into()?));
3741
}
38-
// Try as string
39-
let s = std::str::from_utf8(bytes)?;
42+
// Some drivers may return UUIDs as ASCII/UTF-8 bytes
43+
let s = std::str::from_utf8(bytes)?.trim();
4044
return Ok(Uuid::from_str(s)?);
4145
}
4246
let s = <String as Decode<'r, Odbc>>::decode(value)?;
43-
Ok(Uuid::from_str(&s)?)
47+
Ok(Uuid::from_str(s.trim())?)
4448
}
4549
}

tests/odbc/types.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ test_type!(uuid<sqlx_oldapi::types::Uuid>(Odbc,
103103
"'00000000-0000-0000-0000-000000000000'" == sqlx_oldapi::types::Uuid::nil()
104104
));
105105

106+
// Extra UUID decoding edge cases (ODBC may return padded strings)
107+
#[cfg(feature = "uuid")]
108+
test_type!(uuid_padded<sqlx_oldapi::types::Uuid>(Odbc,
109+
"'550e8400-e29b-41d4-a716-446655440000 '" == sqlx_oldapi::types::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap()
110+
));
111+
106112
#[cfg(feature = "json")]
107113
mod json_tests {
108114
use super::*;
@@ -156,6 +162,11 @@ mod chrono_tests {
156162
"'2019-01-02 05:10:20'" == NaiveDate::from_ymd_opt(2019, 1, 2).unwrap().and_hms_opt(5, 10, 20).unwrap()
157163
));
158164

165+
// Extra chrono decoding edge case (padded timestamp string)
166+
test_type!(chrono_datetime_padded<NaiveDateTime>(Odbc,
167+
"'2023-12-25 14:30:00 '" == NaiveDate::from_ymd_opt(2023, 12, 25).unwrap().and_hms_opt(14, 30, 0).unwrap()
168+
));
169+
159170
test_type!(chrono_datetime_utc<DateTime<Utc>>(Odbc,
160171
"'2023-12-25 14:30:00'" == DateTime::<Utc>::from_naive_utc_and_offset(
161172
NaiveDate::from_ymd_opt(2023, 12, 25).unwrap().and_hms_opt(14, 30, 0).unwrap(),

0 commit comments

Comments
 (0)