Skip to content

Commit e4252f5

Browse files
committed
add support for displaying date time and datetime objects natively
Before, one used to have to cast them to text on the database, which wasn't practical for use with the table component, for instance. Fixes #41
1 parent 6a79f9f commit e4252f5

File tree

3 files changed

+101
-51
lines changed

3 files changed

+101
-51
lines changed

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
@@ -17,7 +17,7 @@ panic = "abort"
1717
codegen-units = 2
1818

1919
[dependencies]
20-
sqlx = { package = "sqlx-oldapi", version = "0.6.8", features = ["any", "runtime-actix-rustls", "sqlite", "postgres", "mysql", "mssql", "chrono"] }
20+
sqlx = { package = "sqlx-oldapi", version = "0.6.9", features = ["any", "runtime-actix-rustls", "sqlite", "postgres", "mysql", "mssql", "chrono"] }
2121
chrono = "0.4.23"
2222
actix-web = { version = "4", features = ["rustls", "cookies"] }
2323
percent-encoding = "2.2.0"

src/webserver/database/mod.rs

Lines changed: 92 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use sqlx::pool::{PoolConnection, PoolOptions};
2626
use sqlx::query::Query;
2727
use sqlx::{
2828
Any, AnyConnection, AnyPool, Arguments, Column, ConnectOptions, Decode, Either, Executor, Row,
29-
Statement,
29+
Statement, ValueRef, TypeInfo,
3030
};
3131

3232
use self::sql::ParsedSQLStatement;
@@ -202,58 +202,108 @@ pub enum DbItem {
202202
Error(anyhow::Error),
203203
}
204204

205+
macro_rules! try_decode_with {
206+
($raw_value:expr, [$ty0:ty], $fn:expr) => {
207+
<$ty0 as Decode<sqlx::any::Any>>::decode($raw_value).map($fn)
208+
};
209+
($raw_value:expr, [$ty0:ty, $($ty:ty),+], $fn:expr) => {
210+
match try_decode_with!($raw_value, [$ty0], $fn) {
211+
Ok(value) => Ok(value),
212+
Err(_) => try_decode_with!($raw_value, [$($ty),+], $fn),
213+
}
214+
};
215+
}
216+
205217
fn row_to_json(row: &AnyRow) -> Value {
206-
use sqlx::{TypeInfo, ValueRef};
207-
use Value::{Null, Object};
218+
use Value::Object;
208219

209220
let columns = row.columns();
210221
let mut map = Map::new();
211222
for col in columns {
212223
let key = col.name().to_string();
213-
let value: Value = match row.try_get_raw(col.ordinal()) {
214-
Ok(raw_value) if !raw_value.is_null() => match raw_value.type_info().name() {
215-
"REAL" | "FLOAT" | "NUMERIC" | "FLOAT4" | "FLOAT8" | "DOUBLE" => {
216-
<f64 as Decode<sqlx::any::Any>>::decode(raw_value)
217-
.unwrap_or(f64::NAN)
218-
.into()
219-
}
220-
"INT8" | "BIGINT" | "INTEGER" => <i64 as Decode<sqlx::any::Any>>::decode(raw_value)
221-
.unwrap_or_default()
222-
.into(),
223-
"INT" | "INT4" => <i32 as Decode<sqlx::any::Any>>::decode(raw_value)
224-
.unwrap_or_default()
225-
.into(),
226-
"INT2" | "SMALLINT" => <i16 as Decode<sqlx::any::Any>>::decode(raw_value)
227-
.unwrap_or_default()
228-
.into(),
229-
"BOOL" | "BOOLEAN" => <bool as Decode<sqlx::any::Any>>::decode(raw_value)
230-
.unwrap_or_default()
231-
.into(),
232-
"JSON" | "JSON[]" | "JSONB" | "JSONB[]" => {
233-
<&[u8] as Decode<sqlx::any::Any>>::decode(raw_value)
234-
.and_then(|rv| {
235-
serde_json::from_slice::<Value>(rv).map_err(|e| {
236-
Box::new(e) as Box<dyn std::error::Error + Sync + Send>
237-
})
238-
})
239-
.unwrap_or_default()
240-
}
241-
// Deserialize as a string by default
242-
_ => <String as Decode<sqlx::any::Any>>::decode(raw_value)
243-
.unwrap_or_default()
244-
.into(),
245-
},
246-
Ok(_null) => Null,
247-
Err(e) => {
248-
log::warn!("Unable to extract value from row: {:?}", e);
249-
Null
250-
}
251-
};
224+
let value: Value = sql_to_json(row, col);
252225
map = add_value_to_map(map, (key, value));
253226
}
254227
Object(map)
255228
}
256229

230+
fn sql_to_json(row: &AnyRow, col: &sqlx::any::AnyColumn) -> Value {
231+
let raw_value_result = row.try_get_raw(col.ordinal());
232+
match raw_value_result {
233+
Ok(raw_value) if !raw_value.is_null() => {
234+
let mut raw_value = Some(raw_value);
235+
log::trace!("Decoding a value of type {:?}", col.type_info().name());
236+
let decoded = sql_nonnull_to_json(|| {
237+
raw_value.take().unwrap_or_else(|| {
238+
row.try_get_raw(col.ordinal()).unwrap()
239+
})
240+
});
241+
log::trace!("Decoded value: {:?}", decoded);
242+
decoded
243+
},
244+
Ok(_null) => Value::Null,
245+
Err(e) => {
246+
log::warn!("Unable to extract value from row: {:?}", e);
247+
Value::Null
248+
}
249+
}
250+
}
251+
252+
fn sql_nonnull_to_json<'r>(mut get_ref: impl FnMut() -> sqlx::any::AnyValueRef<'r>) -> Value {
253+
let raw_value = get_ref();
254+
match raw_value.type_info().name() {
255+
"REAL" | "FLOAT" | "NUMERIC" | "DECIMAL" | "FLOAT4" | "FLOAT8" | "DOUBLE" => {
256+
<f64 as Decode<sqlx::any::Any>>::decode(raw_value)
257+
.unwrap_or(f64::NAN)
258+
.into()
259+
}
260+
"INT8" | "BIGINT" | "INTEGER" => <i64 as Decode<sqlx::any::Any>>::decode(raw_value)
261+
.unwrap_or_default()
262+
.into(),
263+
"INT" | "INT4" => <i32 as Decode<sqlx::any::Any>>::decode(raw_value)
264+
.unwrap_or_default()
265+
.into(),
266+
"INT2" | "SMALLINT" => <i16 as Decode<sqlx::any::Any>>::decode(raw_value)
267+
.unwrap_or_default()
268+
.into(),
269+
"BOOL" | "BOOLEAN" => <bool as Decode<sqlx::any::Any>>::decode(raw_value)
270+
.unwrap_or_default()
271+
.into(),
272+
"DATE" => <chrono::NaiveDate as Decode<sqlx::any::Any>>::decode(raw_value)
273+
.as_ref()
274+
.map(ToString::to_string)
275+
.unwrap_or_else(|e| e.to_string())
276+
.into(),
277+
"TIME" => <chrono::NaiveTime as Decode<sqlx::any::Any>>::decode(raw_value)
278+
.as_ref()
279+
.map(ToString::to_string)
280+
.unwrap_or_else(|e| e.to_string())
281+
.into(),
282+
"DATETIME" | "DATETIME2" | "DATETIMEOFFSET" | "TIMESTAMP" | "TIMESTAMPTZ" => {
283+
try_decode_with!(
284+
get_ref(),
285+
[chrono::NaiveDateTime, chrono::DateTime<chrono::Utc>],
286+
|v| dbg!(v).to_string()
287+
)
288+
.unwrap_or_else(|e| format!("Unable to decode date: {:?}", e))
289+
.into()
290+
}
291+
"JSON" | "JSON[]" | "JSONB" | "JSONB[]" => {
292+
<&[u8] as Decode<sqlx::any::Any>>::decode(raw_value)
293+
.and_then(|rv| {
294+
serde_json::from_slice::<Value>(rv).map_err(|e| {
295+
Box::new(e) as Box<dyn std::error::Error + Sync + Send>
296+
})
297+
})
298+
.unwrap_or_default()
299+
}
300+
// Deserialize as a string by default
301+
_ => <String as Decode<sqlx::any::Any>>::decode(raw_value)
302+
.unwrap_or_default()
303+
.into(),
304+
}
305+
}
306+
257307
impl Database {
258308
pub async fn init(config: &AppConfig) -> anyhow::Result<Self> {
259309
let database_url = &config.database_url;

0 commit comments

Comments
 (0)