Skip to content
Draft
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
15 changes: 15 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ path = "tests/named-instance-smol.rs"
name = "named-instance-smol"
required-features = ["sql-browser-smol"]

[[test]]
path = "tests/serde.rs"
name = "serde"
required-features = ["serde"]

[dependencies]
enumflags2 = "0.7"
byteorder = "1.0"
Expand Down Expand Up @@ -105,6 +110,11 @@ version = "0.3"
optional = true
package = "bigdecimal"

[dependencies.serde]
version = "1.0"
optional = true
features = ["derive", "rc"]

[dependencies.async-io]
version = "1.8"
optional = true
Expand Down Expand Up @@ -174,6 +184,7 @@ paste = "1.0"
indicatif = "0.17"
chrono = "0.4.38"
indoc = "1.0.7"
serde_json = "1.0"

[package.metadata.docs.rs]
features = ["all", "docs"]
Expand All @@ -190,6 +201,7 @@ all = [
"rust_decimal",
"bigdecimal",
"native-tls",
"serde",
]
default = ["tds73", "winauth", "native-tls"]
tds73 = []
Expand All @@ -202,3 +214,6 @@ bigdecimal = ["bigdecimal_"]
rustls = ["tokio-rustls", "tokio-util", "rustls-pemfile", "rustls-native-certs"]
native-tls = ["async-native-tls"]
vendored-openssl = ["opentls"]
# Optional serde Serialize/Deserialize impls for query result types
# (Row, Column, ColumnData, Numeric, ColumnType and time/xml types).
serde = ["dep:serde", "uuid/serde"]
3 changes: 3 additions & 0 deletions src/row.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::{fmt::Display, sync::Arc};

/// A column of data from a query.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Column {
pub(crate) name: String,
pub(crate) column_type: ColumnType,
Expand All @@ -30,6 +31,7 @@ impl Column {
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
/// The type of the column.
pub enum ColumnType {
/// The column doesn't have a specified type.
Expand Down Expand Up @@ -246,6 +248,7 @@ impl From<&TypeInfo> for ColumnType {
/// [`try_get`]: #method.try_get
/// [`IntoIterator`]: #impl-IntoIterator
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Row {
pub(crate) columns: Arc<Vec<Column>>,
pub(crate) data: TokenRow<'static>,
Expand Down
1 change: 1 addition & 0 deletions src/tds/codec/column_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use uuid::Uuid;
const MAX_NVARCHAR_SIZE: usize = 1 << 30;

#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
/// A container of a value that can be represented as a TDS value.
pub enum ColumnData<'a> {
/// 8-bit integer, unsigned.
Expand Down
1 change: 1 addition & 0 deletions src/tds/codec/token/token_row.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub use into_row::IntoRow;

/// A row of data.
#[derive(Debug, Default, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TokenRow<'a> {
data: Vec<ColumnData<'a>>,
}
Expand Down
1 change: 1 addition & 0 deletions src/tds/numeric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use std::fmt::{self, Debug, Display, Formatter};
/// A recommended way of dealing with numeric values is by enabling the
/// `rust_decimal` feature and using its `Decimal` type instead.
#[derive(Copy, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Numeric {
value: i128,
scale: u8,
Expand Down
6 changes: 6 additions & 0 deletions src/tds/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ use futures_util::io::AsyncReadExt;
/// It isn't recommended to use this type directly. For dealing with `datetime`,
/// use the `time` feature of this crate and its `PrimitiveDateTime` type.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DateTime {
days: i32,
seconds_fragments: u32,
Expand Down Expand Up @@ -99,6 +100,7 @@ impl Encode<BytesMut> for DateTime {
/// `smalldatetime`, use the `time` feature of this crate and its
/// `PrimitiveDateTime` type.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SmallDateTime {
days: u16,
seconds_fragments: u16,
Expand Down Expand Up @@ -152,6 +154,7 @@ impl Encode<BytesMut> for SmallDateTime {
/// It isn't recommended to use this type directly. If you want to deal with
/// `date`, use the `time` feature of this crate and its `Date` type.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg(feature = "tds73")]
#[cfg_attr(feature = "docs", doc(cfg(feature = "tds73")))]
pub struct Date(u32);
Expand Down Expand Up @@ -205,6 +208,7 @@ impl Encode<BytesMut> for Date {
/// It isn't recommended to use this type directly. If you want to deal with
/// `time`, use the `time` feature of this crate and its `Time` type.
#[derive(Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg(feature = "tds73")]
#[cfg_attr(feature = "docs", doc(cfg(feature = "tds73")))]
pub struct Time {
Expand Down Expand Up @@ -318,6 +322,7 @@ impl Encode<BytesMut> for Time {
}

#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg(feature = "tds73")]
#[cfg_attr(feature = "docs", doc(cfg(feature = "tds73")))]
/// A presentation of `datetime2` type in the server.
Expand Down Expand Up @@ -380,6 +385,7 @@ impl Encode<BytesMut> for DateTime2 {
}

#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg(feature = "tds73")]
#[cfg_attr(feature = "docs", doc(cfg(feature = "tds73")))]
/// A presentation of `datetimeoffset` type in the server.
Expand Down
2 changes: 2 additions & 0 deletions src/tds/xml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::sync::Arc;

/// Provides information of the location for the schema.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct XmlSchema {
db_name: String,
owner: String,
Expand Down Expand Up @@ -45,6 +46,7 @@ impl XmlSchema {
/// A representation of XML data in TDS. Holds the data as a UTF-8 string and
/// and optional information about the schema.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct XmlData {
data: String,
schema: Option<Arc<XmlSchema>>,
Expand Down
195 changes: 195 additions & 0 deletions tests/serde.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
//! Tests that verify the optional serde Serialize/Deserialize impls
//! gated behind the `serde` feature.
//!
//! These tests do not require a live SQL Server. They round-trip the
//! exposed result-set types (Row pieces, ColumnData, Numeric, time
//! types, etc.) through `serde_json` and assert the values survive.

#![cfg(feature = "serde")]

use std::borrow::Cow;
use std::sync::Arc;

use tiberius::numeric::Numeric;
use tiberius::time::DateTime;
use tiberius::xml::XmlData;
use tiberius::{Column, ColumnData, ColumnType, TokenRow};
use uuid::Uuid;

#[cfg(feature = "tds73")]
use tiberius::time::{Date, DateTime2, DateTimeOffset, Time};

fn json_round_trip<T>(value: &T) -> T
where
T: serde::Serialize + serde::de::DeserializeOwned,
{
let s = serde_json::to_string(value).expect("serialize");
serde_json::from_str(&s).expect("deserialize")
}

#[test]
fn column_type_round_trip() {
let value = ColumnType::Int4;
let back: ColumnType = json_round_trip(&value);
assert_eq!(value, back);
}

#[test]
fn column_round_trip() {
let value = Column::new("hello".to_string(), ColumnType::NVarchar);
let back: Column = json_round_trip(&value);
assert_eq!(back.name(), "hello");
assert_eq!(back.column_type(), ColumnType::NVarchar);
}

#[test]
fn column_data_int_round_trip() {
let value = ColumnData::I32(Some(42));
let back: ColumnData<'static> = json_round_trip(&value);
assert_eq!(value, back);
}

#[test]
fn column_data_null_round_trip() {
let value: ColumnData<'static> = ColumnData::I64(None);
let back: ColumnData<'static> = json_round_trip(&value);
assert_eq!(value, back);
}

#[test]
fn column_data_string_round_trip() {
let value = ColumnData::String(Some(Cow::Borrowed("héllo")));
let back: ColumnData<'static> = json_round_trip(&value);
// Borrowed inputs deserialize as Cow::Owned but values match.
match back {
ColumnData::String(Some(s)) => assert_eq!(s.as_ref(), "héllo"),
other => panic!("unexpected: {:?}", other),
}
}

#[test]
fn column_data_binary_round_trip() {
let bytes: &[u8] = &[0xde, 0xad, 0xbe, 0xef];
let value = ColumnData::Binary(Some(Cow::Borrowed(bytes)));
let back: ColumnData<'static> = json_round_trip(&value);
match back {
ColumnData::Binary(Some(b)) => assert_eq!(b.as_ref(), bytes),
other => panic!("unexpected: {:?}", other),
}
}

#[test]
fn column_data_guid_round_trip() {
let id = Uuid::from_u128(0xfeed_face_dead_beef_0000_1111_2222_3333u128);
let value = ColumnData::Guid(Some(id));
let back: ColumnData<'static> = json_round_trip(&value);
assert_eq!(value, back);
}

#[test]
fn column_data_bool_round_trip() {
let value = ColumnData::Bit(Some(true));
let back: ColumnData<'static> = json_round_trip(&value);
assert_eq!(value, back);
}

#[test]
fn column_data_float_round_trip() {
let value = ColumnData::F64(Some(std::f64::consts::PI));
let back: ColumnData<'static> = json_round_trip(&value);
assert_eq!(value, back);
}

#[test]
fn numeric_round_trip() {
let value = Numeric::new_with_scale(57705, 2);
let back: Numeric = json_round_trip(&value);
assert_eq!(value, back);
assert_eq!(back.value(), 57705);
assert_eq!(back.scale(), 2);
}

#[test]
fn column_data_numeric_round_trip() {
let value = ColumnData::Numeric(Some(Numeric::new_with_scale(12345, 3)));
let back: ColumnData<'static> = json_round_trip(&value);
assert_eq!(value, back);
}

#[test]
fn datetime_round_trip() {
let value = DateTime::new(200, 3000);
let back: DateTime = json_round_trip(&value);
assert_eq!(value, back);
}

#[test]
fn column_data_datetime_round_trip() {
let value = ColumnData::DateTime(Some(DateTime::new(42, 84)));
let back: ColumnData<'static> = json_round_trip(&value);
assert_eq!(value, back);
}

#[cfg(feature = "tds73")]
#[test]
fn time_types_round_trip() {
let date = Date::new(123);
let time = Time::new(7, 7);
let dt2 = DateTime2::new(date, time);
let dto = DateTimeOffset::new(dt2, -120);

assert_eq!(date, json_round_trip(&date));
assert_eq!(time, json_round_trip(&time));
assert_eq!(dt2, json_round_trip(&dt2));
assert_eq!(dto, json_round_trip(&dto));
}

#[test]
fn xml_data_round_trip() {
let value = XmlData::new("<root>hi</root>");
let back: XmlData = json_round_trip(&value);
assert_eq!(value.as_ref(), back.as_ref());
}

#[test]
fn token_row_round_trip() {
let mut row: TokenRow<'static> = TokenRow::new();
row.push(ColumnData::I32(Some(1)));
row.push(ColumnData::String(Some(Cow::Owned("hello".to_string()))));
row.push(ColumnData::Bit(Some(false)));

let back: TokenRow<'static> = json_round_trip(&row);
assert_eq!(back.len(), 3);
assert_eq!(back.get(0), Some(&ColumnData::I32(Some(1))));
match back.get(1).unwrap() {
ColumnData::String(Some(s)) => assert_eq!(s.as_ref(), "hello"),
other => panic!("unexpected: {:?}", other),
}
assert_eq!(back.get(2), Some(&ColumnData::Bit(Some(false))));
}

/// Mimic the "send query results across the network as JSON" flow from
/// the issue: build a row out of columns + data, then verify the whole
/// thing round-trips.
#[test]
fn row_shape_round_trip() {
// Build column metadata.
let columns = Arc::new(vec![
Column::new("id".to_string(), ColumnType::Int4),
Column::new("name".to_string(), ColumnType::NVarchar),
]);
let mut data: TokenRow<'static> = TokenRow::new();
data.push(ColumnData::I32(Some(7)));
data.push(ColumnData::String(Some(Cow::Owned("ada".to_string()))));

// Round-trip the column metadata.
let columns_back: Arc<Vec<Column>> = json_round_trip(&columns);
assert_eq!(columns_back.len(), 2);
assert_eq!(columns_back[0].name(), "id");
assert_eq!(columns_back[1].column_type(), ColumnType::NVarchar);

// Round-trip the row data.
let data_back: TokenRow<'static> = json_round_trip(&data);
assert_eq!(data_back.len(), 2);
assert_eq!(data_back.get(0), Some(&ColumnData::I32(Some(7))));
}