Skip to content

Commit 552ef26

Browse files
committed
better and simpler sql to json decoding
sane time zone handling
1 parent f556af1 commit 552ef26

File tree

4 files changed

+53
-58
lines changed

4 files changed

+53
-58
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Added support for `BIT` columns in Microsoft SQL Server.
1010
- Avoid generating file names that contain spaces in `sqlpage.persist_uploaded_file`. This makes it easier to use the file name in URLs without URL-encoding it.
1111
- Fixed a bug with REAL value decoding in Microsoft SQL Server.
12+
- Support preserving the timezone of `DATETIMEOFFSET` columns in Microsoft SQL Server and `TIMESTAMPTZ` columns in Postgres. Previously, all datetime columns were converted to UTC.
1213

1314
## 0.30.1 (2024-10-31)
1415
- fix a bug where table sorting would break if table search was not also enabled.

Cargo.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ panic = "abort"
1818
codegen-units = 2
1919

2020
[dependencies]
21-
sqlx = { package = "sqlx-oldapi", version = "0.6.33", features = [
21+
sqlx = { package = "sqlx-oldapi", version = "0.6.34", features = [
2222
"any",
2323
"runtime-actix-rustls",
2424
"sqlite",

src/webserver/database/sql_to_json.rs

Lines changed: 43 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::utils::add_value_to_map;
2-
use chrono::{DateTime, Utc};
2+
use chrono::{DateTime, FixedOffset, NaiveDateTime};
33
use serde_json::{self, Map, Value};
44
use sqlx::any::{AnyRow, AnyTypeInfo, AnyTypeInfoKind};
55
use sqlx::Decode;
@@ -39,62 +39,54 @@ pub fn sql_to_json(row: &AnyRow, col: &sqlx::any::AnyColumn) -> Value {
3939
}
4040
}
4141

42+
fn decode_raw<'a, T: Decode<'a, sqlx::any::Any> + Default>(
43+
raw_value: sqlx::any::AnyValueRef<'a>,
44+
) -> T {
45+
match T::decode(raw_value) {
46+
Ok(v) => v,
47+
Err(e) => {
48+
let type_name = std::any::type_name::<T>();
49+
log::error!("Failed to decode {type_name} value: {e}");
50+
T::default()
51+
}
52+
}
53+
}
54+
4255
pub fn sql_nonnull_to_json<'r>(mut get_ref: impl FnMut() -> sqlx::any::AnyValueRef<'r>) -> Value {
4356
let raw_value = get_ref();
4457
let type_info = raw_value.type_info();
4558
let type_name = type_info.name();
4659
log::trace!("Decoding a value of type {:?}", type_name);
4760
match type_name {
4861
"REAL" | "FLOAT" | "FLOAT4" | "FLOAT8" | "DOUBLE" | "NUMERIC" | "DECIMAL" => {
49-
<f64 as Decode<sqlx::any::Any>>::decode(raw_value)
50-
.unwrap_or(f64::NAN)
51-
.into()
62+
decode_raw::<f64>(raw_value).into()
5263
}
5364
"INT8" | "BIGINT" | "SERIAL8" | "BIGSERIAL" | "IDENTITY" | "INT64" | "INTEGER8"
54-
| "BIGINT UNSIGNED" | "BIGINT SIGNED" => <i64 as Decode<sqlx::any::Any>>::decode(raw_value)
55-
.unwrap_or_default()
56-
.into(),
57-
"INT" | "INT4" | "INTEGER" => <i32 as Decode<sqlx::any::Any>>::decode(raw_value)
58-
.unwrap_or_default()
59-
.into(),
60-
"INT2" | "SMALLINT" => <i16 as Decode<sqlx::any::Any>>::decode(raw_value)
61-
.unwrap_or_default()
62-
.into(),
63-
"BOOL" | "BOOLEAN" => <bool as Decode<sqlx::any::Any>>::decode(raw_value)
64-
.unwrap_or_default()
65-
.into(),
65+
| "BIGINT UNSIGNED" | "BIGINT SIGNED" => decode_raw::<i64>(raw_value).into(),
66+
"INT" | "INT4" | "INTEGER" => decode_raw::<i32>(raw_value).into(),
67+
"INT2" | "SMALLINT" => decode_raw::<i16>(raw_value).into(),
68+
"BOOL" | "BOOLEAN" => decode_raw::<bool>(raw_value).into(),
6669
"BIT" if matches!(*type_info, AnyTypeInfo(AnyTypeInfoKind::Mssql(_))) => {
67-
<bool as Decode<sqlx::any::Any>>::decode(raw_value)
68-
.unwrap_or_default()
69-
.into()
70+
decode_raw::<bool>(raw_value).into()
7071
}
71-
"DATE" => <chrono::NaiveDate as Decode<sqlx::any::Any>>::decode(raw_value)
72-
.as_ref()
73-
.map_or_else(std::string::ToString::to_string, ToString::to_string)
72+
"DATE" => decode_raw::<chrono::NaiveDate>(raw_value)
73+
.to_string()
7474
.into(),
75-
"TIME" | "TIMETZ" => <chrono::NaiveTime as Decode<sqlx::any::Any>>::decode(raw_value)
76-
.as_ref()
77-
.map_or_else(ToString::to_string, ToString::to_string)
75+
"TIME" | "TIMETZ" => decode_raw::<chrono::NaiveTime>(raw_value)
76+
.to_string()
7877
.into(),
79-
"DATETIME" | "DATETIME2" | "DATETIMEOFFSET" | "TIMESTAMP" | "TIMESTAMPTZ" => {
80-
let mut date_time = <DateTime<Utc> as Decode<sqlx::any::Any>>::decode(get_ref());
81-
if date_time.is_err() {
82-
date_time = <chrono::NaiveDateTime as Decode<sqlx::any::Any>>::decode(raw_value)
83-
.map(|d| d.and_utc());
84-
}
85-
Value::String(
86-
date_time
87-
.as_ref()
88-
.map_or_else(ToString::to_string, DateTime::to_rfc3339),
89-
)
90-
}
91-
"JSON" | "JSON[]" | "JSONB" | "JSONB[]" => {
92-
<Value as Decode<sqlx::any::Any>>::decode(raw_value).unwrap_or_default()
78+
"DATETIMEOFFSET" | "TIMESTAMP" | "TIMESTAMPTZ" => {
79+
decode_raw::<DateTime<FixedOffset>>(raw_value)
80+
.to_rfc3339()
81+
.into()
9382
}
94-
// Deserialize as a string by default
95-
_ => <String as Decode<sqlx::any::Any>>::decode(raw_value)
96-
.unwrap_or_default()
83+
"DATETIME" | "DATETIME2" => decode_raw::<NaiveDateTime>(raw_value)
84+
.format("%FT%T%.f")
85+
.to_string()
9786
.into(),
87+
"JSON" | "JSON[]" | "JSONB" | "JSONB[]" => decode_raw::<Value>(raw_value),
88+
// Deserialize as a string by default
89+
_ => decode_raw::<String>(raw_value).into(),
9890
}
9991
}
10092

@@ -170,7 +162,8 @@ mod tests {
170162
'2024-03-14'::DATE as date,
171163
'13:14:15'::TIME as time,
172164
'2024-03-14 13:14:15'::TIMESTAMP as timestamp,
173-
'2024-03-14 13:14:15+00'::TIMESTAMPTZ as timestamptz,
165+
'2024-03-14 13:14:15+02:00'::TIMESTAMPTZ as timestamptz,
166+
INTERVAL '1 day' as interval,
174167
'{\"key\": \"value\"}'::JSON as json,
175168
'{\"key\": \"value\"}'::JSONB as jsonb",
176169
)
@@ -189,7 +182,8 @@ mod tests {
189182
"date": "2024-03-14",
190183
"time": "13:14:15",
191184
"timestamp": "2024-03-14T13:14:15+00:00",
192-
"timestamptz": "2024-03-14T13:14:15+00:00",
185+
"timestamptz": "2024-03-14T11:14:15+00:00", // Postgres stores all timestamps in UTC
186+
"interval": "1 day",
193187
"json": {"key": "value"},
194188
"jsonb": {"key": "value"},
195189
})
@@ -225,7 +219,7 @@ mod tests {
225219
"decimal_number": 42.25,
226220
"date": "2024-03-14",
227221
"time": "13:14:15",
228-
"datetime": "2024-03-14T13:14:15+00:00",
222+
"datetime": "2024-03-14T13:14:15",
229223
"hex_value": "hello world",
230224
"json": {"key": "value"},
231225
})
@@ -282,7 +276,7 @@ mod tests {
282276
CAST('13:14:15' AS TIME) as time,
283277
CAST('2024-03-14 13:14:15' AS DATETIME) as datetime,
284278
CAST('2024-03-14 13:14:15' AS DATETIME2) as datetime2,
285-
CAST('2024-03-14 13:14:15 +00:00' AS DATETIMEOFFSET) as datetimeoffset,
279+
CAST('2024-03-14 13:14:15 +02:00' AS DATETIMEOFFSET) as datetimeoffset,
286280
N'Unicode String' as nvarchar,
287281
'ASCII String' as varchar",
288282
)
@@ -303,9 +297,9 @@ mod tests {
303297
"decimal": 42.25,
304298
"date": "2024-03-14",
305299
"time": "13:14:15",
306-
"datetime": "2024-03-14T13:14:15+00:00",
307-
"datetime2": "2024-03-14T13:14:15+00:00",
308-
"datetimeoffset": "2024-03-14T13:14:15+00:00",
300+
"datetime": "2024-03-14T13:14:15",
301+
"datetime2": "2024-03-14T13:14:15",
302+
"datetimeoffset": "2024-03-14T13:14:15+02:00",
309303
"nvarchar": "Unicode String",
310304
"varchar": "ASCII String",
311305
})

0 commit comments

Comments
 (0)