mirror of
https://github.com/launchbadge/sqlx
synced 2024-11-10 14:34:19 +00:00
392 lines
11 KiB
Rust
392 lines
11 KiB
Rust
use std::sync::atomic::{AtomicU32, Ordering};
|
|
|
|
use sqlx::decode::Decode;
|
|
use sqlx::encode::Encode;
|
|
use sqlx::postgres::types::raw::{PgNumeric, PgNumericSign, PgRecordDecoder, PgRecordEncoder};
|
|
use sqlx::postgres::{PgQueryAs, PgTypeInfo, PgValue};
|
|
use sqlx::{Cursor, Executor, Postgres, Row, Type};
|
|
use sqlx_test::{new, test_prepared_type, test_type};
|
|
|
|
test_type!(null(
|
|
Postgres,
|
|
Option<i16>,
|
|
"NULL" == None::<i16>
|
|
));
|
|
|
|
test_type!(bool(
|
|
Postgres,
|
|
bool,
|
|
"false::boolean" == false,
|
|
"true::boolean" == true
|
|
));
|
|
|
|
test_type!(i16(Postgres, i16, "821::smallint" == 821_i16));
|
|
test_type!(i32(Postgres, i32, "94101::int" == 94101_i32));
|
|
test_type!(i64(Postgres, i64, "9358295312::bigint" == 9358295312_i64));
|
|
|
|
test_type!(f32(Postgres, f32, "9419.122::real" == 9419.122_f32));
|
|
test_type!(f64(
|
|
Postgres,
|
|
f64,
|
|
"939399419.1225182::double precision" == 939399419.1225182_f64
|
|
));
|
|
|
|
test_type!(string(
|
|
Postgres,
|
|
String,
|
|
"'this is foo'" == "this is foo",
|
|
"''" == ""
|
|
));
|
|
|
|
test_type!(bytea(
|
|
Postgres,
|
|
Vec<u8>,
|
|
"E'\\\\xDEADBEEF'::bytea"
|
|
== vec![0xDE_u8, 0xAD, 0xBE, 0xEF],
|
|
"E'\\\\x'::bytea"
|
|
== Vec::<u8>::new(),
|
|
"E'\\\\x0000000052'::bytea"
|
|
== vec![0_u8, 0, 0, 0, 0x52]
|
|
));
|
|
|
|
// PgNumeric only works on the wire protocol
|
|
test_prepared_type!(numeric(
|
|
Postgres,
|
|
PgNumeric,
|
|
"0::numeric"
|
|
== PgNumeric::Number {
|
|
sign: PgNumericSign::Positive,
|
|
weight: 0,
|
|
scale: 0,
|
|
digits: vec![]
|
|
},
|
|
"(-0)::numeric"
|
|
== PgNumeric::Number {
|
|
sign: PgNumericSign::Positive,
|
|
weight: 0,
|
|
scale: 0,
|
|
digits: vec![]
|
|
},
|
|
"1::numeric"
|
|
== PgNumeric::Number {
|
|
sign: PgNumericSign::Positive,
|
|
weight: 0,
|
|
scale: 0,
|
|
digits: vec![1]
|
|
},
|
|
"1234::numeric"
|
|
== PgNumeric::Number {
|
|
sign: PgNumericSign::Positive,
|
|
weight: 0,
|
|
scale: 0,
|
|
digits: vec![1234]
|
|
},
|
|
"10000::numeric"
|
|
== PgNumeric::Number {
|
|
sign: PgNumericSign::Positive,
|
|
weight: 1,
|
|
scale: 0,
|
|
digits: vec![1]
|
|
},
|
|
"0.1::numeric"
|
|
== PgNumeric::Number {
|
|
sign: PgNumericSign::Positive,
|
|
weight: -1,
|
|
scale: 1,
|
|
digits: vec![1000]
|
|
},
|
|
"0.01234::numeric"
|
|
== PgNumeric::Number {
|
|
sign: PgNumericSign::Positive,
|
|
weight: -1,
|
|
scale: 5,
|
|
digits: vec![123, 4000]
|
|
},
|
|
"12.34::numeric"
|
|
== PgNumeric::Number {
|
|
sign: PgNumericSign::Positive,
|
|
weight: 0,
|
|
scale: 2,
|
|
digits: vec![12, 3400]
|
|
},
|
|
"'NaN'::numeric" == PgNumeric::NotANumber,
|
|
));
|
|
|
|
#[cfg(feature = "bigdecimal")]
|
|
test_type!(decimal(
|
|
Postgres,
|
|
sqlx::types::BigDecimal,
|
|
"1::numeric" == "1".parse::<sqlx::types::BigDecimal>().unwrap(),
|
|
"10000::numeric" == "10000".parse::<sqlx::types::BigDecimal>().unwrap(),
|
|
"0.1::numeric" == "0.1".parse::<sqlx::types::BigDecimal>().unwrap(),
|
|
"0.01234::numeric" == "0.01234".parse::<sqlx::types::BigDecimal>().unwrap(),
|
|
"12.34::numeric" == "12.34".parse::<sqlx::types::BigDecimal>().unwrap(),
|
|
"12345.6789::numeric" == "12345.6789".parse::<sqlx::types::BigDecimal>().unwrap(),
|
|
));
|
|
|
|
#[cfg(feature = "uuid")]
|
|
test_type!(uuid(
|
|
Postgres,
|
|
sqlx::types::Uuid,
|
|
"'b731678f-636f-4135-bc6f-19440c13bd19'::uuid"
|
|
== sqlx::types::Uuid::parse_str("b731678f-636f-4135-bc6f-19440c13bd19").unwrap(),
|
|
"'00000000-0000-0000-0000-000000000000'::uuid"
|
|
== sqlx::types::Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap()
|
|
));
|
|
|
|
#[cfg(feature = "ipnetwork")]
|
|
test_type!(ipnetwork(
|
|
Postgres,
|
|
sqlx::types::ipnetwork::IpNetwork,
|
|
"'127.0.0.1'::inet"
|
|
== "127.0.0.1"
|
|
.parse::<sqlx::types::ipnetwork::IpNetwork>()
|
|
.unwrap(),
|
|
"'8.8.8.8/24'::inet"
|
|
== "8.8.8.8/24"
|
|
.parse::<sqlx::types::ipnetwork::IpNetwork>()
|
|
.unwrap(),
|
|
"'::ffff:1.2.3.0'::inet"
|
|
== "::ffff:1.2.3.0"
|
|
.parse::<sqlx::types::ipnetwork::IpNetwork>()
|
|
.unwrap(),
|
|
"'2001:4f8:3:ba::/64'::inet"
|
|
== "2001:4f8:3:ba::/64"
|
|
.parse::<sqlx::types::ipnetwork::IpNetwork>()
|
|
.unwrap(),
|
|
"'192.168'::cidr"
|
|
== "192.168.0.0/24"
|
|
.parse::<sqlx::types::ipnetwork::IpNetwork>()
|
|
.unwrap(),
|
|
"'::ffff:1.2.3.0/120'::cidr"
|
|
== "::ffff:1.2.3.0/120"
|
|
.parse::<sqlx::types::ipnetwork::IpNetwork>()
|
|
.unwrap(),
|
|
));
|
|
|
|
#[cfg(feature = "chrono")]
|
|
mod chrono {
|
|
use sqlx::types::chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
|
|
|
|
use super::*;
|
|
|
|
test_type!(chrono_date(
|
|
Postgres,
|
|
NaiveDate,
|
|
"DATE '2001-01-05'" == NaiveDate::from_ymd(2001, 1, 5),
|
|
"DATE '2050-11-23'" == NaiveDate::from_ymd(2050, 11, 23)
|
|
));
|
|
|
|
test_type!(chrono_time(
|
|
Postgres,
|
|
NaiveTime,
|
|
"TIME '05:10:20.115100'" == NaiveTime::from_hms_micro(5, 10, 20, 115100)
|
|
));
|
|
|
|
test_type!(chrono_date_time(
|
|
Postgres,
|
|
NaiveDateTime,
|
|
"'2019-01-02 05:10:20'::timestamp" == NaiveDate::from_ymd(2019, 1, 2).and_hms(5, 10, 20)
|
|
));
|
|
|
|
test_type!(chrono_date_time_tz(
|
|
Postgres,
|
|
DateTime::<Utc>,
|
|
"TIMESTAMPTZ '2019-01-02 05:10:20.115100'"
|
|
== DateTime::<Utc>::from_utc(
|
|
NaiveDate::from_ymd(2019, 1, 2).and_hms_micro(5, 10, 20, 115100),
|
|
Utc,
|
|
)
|
|
));
|
|
}
|
|
|
|
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
|
|
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
|
|
async fn test_prepared_anonymous_record() -> anyhow::Result<()> {
|
|
let mut conn = new::<Postgres>().await?;
|
|
|
|
// Tuple of no elements is not possible
|
|
// Tuple of 1 element requires a concrete type
|
|
// Tuple with a NULL requires a concrete type
|
|
|
|
// Tuple of 2 elements
|
|
let rec: ((bool, i32),) = sqlx::query_as("SELECT (true, 23512)")
|
|
.fetch_one(&mut conn)
|
|
.await?;
|
|
|
|
assert_eq!((rec.0).0, true);
|
|
assert_eq!((rec.0).1, 23512);
|
|
|
|
// Tuple with an empty string
|
|
let rec: ((bool, String),) = sqlx::query_as("SELECT (true,'')")
|
|
.fetch_one(&mut conn)
|
|
.await?;
|
|
|
|
assert_eq!((rec.0).1, "");
|
|
|
|
// Tuple with a string with an interior comma
|
|
let rec: ((bool, String),) = sqlx::query_as("SELECT (true,'Hello, World!')")
|
|
.fetch_one(&mut conn)
|
|
.await?;
|
|
|
|
assert_eq!((rec.0).1, "Hello, World!");
|
|
|
|
// Tuple with a string with an emoji
|
|
let rec: ((bool, String),) = sqlx::query_as("SELECT (true,'Hello, 🐕!')")
|
|
.fetch_one(&mut conn)
|
|
.await?;
|
|
|
|
assert_eq!((rec.0).1, "Hello, 🐕!");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
|
|
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
|
|
async fn test_unprepared_anonymous_record() -> anyhow::Result<()> {
|
|
let mut conn = new::<Postgres>().await?;
|
|
|
|
// Tuple of no elements is not possible
|
|
// Tuple of 1 element requires a concrete type
|
|
// Tuple with a NULL requires a concrete type
|
|
|
|
// Tuple of 2 elements
|
|
let mut cursor = conn.fetch("SELECT (true, 23512)");
|
|
let row = cursor.next().await?.unwrap();
|
|
let rec: (bool, i32) = row.get(0);
|
|
|
|
assert_eq!(rec.0, true);
|
|
assert_eq!(rec.1, 23512);
|
|
|
|
// Tuple with an empty string
|
|
let mut cursor = conn.fetch("SELECT (true, '')");
|
|
let row = cursor.next().await?.unwrap();
|
|
let rec: (bool, String) = row.get(0);
|
|
|
|
assert_eq!(rec.1, "");
|
|
|
|
// Tuple with a string with an interior comma
|
|
let mut cursor = conn.fetch("SELECT (true, 'Hello, World!')");
|
|
let row = cursor.next().await?.unwrap();
|
|
let rec: (bool, String) = row.get(0);
|
|
|
|
assert_eq!(rec.1, "Hello, World!");
|
|
|
|
// Tuple with a string with an emoji
|
|
let mut cursor = conn.fetch("SELECT (true, 'Hello, 🐕!')");
|
|
let row = cursor.next().await?.unwrap();
|
|
let rec: (bool, String) = row.get(0);
|
|
|
|
assert_eq!(rec.1, "Hello, 🐕!");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
test!(postgres_int_vec: Vec<i32>: "ARRAY[1, 2, 3]::int[]" == vec![1, 2, 3i32], "ARRAY[3, 292, 15, 2, 3]::int[]" == vec![3, 292, 15, 2, 3], "ARRAY[7, 6, 5, 4, 3, 2, 1]::int[]" == vec![7, 6, 5, 4, 3, 2, 1], "ARRAY[]::int[]" == vec![] as Vec<i32>);
|
|
test!(postgres_string_vec: Vec<String>: "ARRAY['Hello', 'world', 'friend']::text[]" == vec!["Hello", "world", "friend"]);
|
|
test!(postgres_bool_vec: Vec<bool>: "ARRAY[true, true, false, true]::bool[]" == vec![true, true, false, true]);
|
|
test!(postgres_real_vec: Vec<f32>: "ARRAY[0.0, 1.0, 3.14, 1.234, -0.002, 100000.0]::real[]" == vec![0.0, 1.0, 3.14, 1.234, -0.002, 100000.0_f32]);
|
|
test!(postgres_double_vec: Vec<f64>: "ARRAY[0.0, 1.0, 3.14, 1.234, -0.002, 100000.0]::double precision[]" == vec![0.0, 1.0, 3.14, 1.234, -0.002, 100000.0_f64]);
|
|
|
|
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
|
|
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
|
|
async fn test_prepared_structs() -> anyhow::Result<()> {
|
|
let mut conn = new::<Postgres>().await?;
|
|
|
|
//
|
|
// Setup custom types if needed
|
|
//
|
|
|
|
static OID_RECORD_EMPTY: AtomicU32 = AtomicU32::new(0);
|
|
static OID_RECORD_1: AtomicU32 = AtomicU32::new(0);
|
|
|
|
conn.execute(
|
|
r#"
|
|
DO $$ BEGIN
|
|
CREATE TYPE _sqlx_record_empty AS ();
|
|
CREATE TYPE _sqlx_record_1 AS (_1 int8);
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
"#,
|
|
)
|
|
.await?;
|
|
|
|
let type_ids: Vec<(i32,)> = sqlx::query_as(
|
|
"SELECT oid::int4 FROM pg_type WHERE typname IN ('_sqlx_record_empty', '_sqlx_record_1')",
|
|
)
|
|
.fetch_all(&mut conn)
|
|
.await?;
|
|
|
|
OID_RECORD_EMPTY.store(type_ids[0].0 as u32, Ordering::SeqCst);
|
|
OID_RECORD_1.store(type_ids[1].0 as u32, Ordering::SeqCst);
|
|
|
|
//
|
|
// Record of no elements
|
|
//
|
|
|
|
struct RecordEmpty {}
|
|
|
|
impl Type<Postgres> for RecordEmpty {
|
|
fn type_info() -> PgTypeInfo {
|
|
PgTypeInfo::with_oid(OID_RECORD_EMPTY.load(Ordering::SeqCst))
|
|
}
|
|
}
|
|
|
|
impl Encode<Postgres> for RecordEmpty {
|
|
fn encode(&self, buf: &mut Vec<u8>) {
|
|
PgRecordEncoder::new(buf).finish();
|
|
}
|
|
}
|
|
|
|
impl<'de> Decode<'de, Postgres> for RecordEmpty {
|
|
fn decode(_value: Option<PgValue<'de>>) -> sqlx::Result<Self> {
|
|
Ok(RecordEmpty {})
|
|
}
|
|
}
|
|
|
|
let _: (RecordEmpty, RecordEmpty) = sqlx::query_as("SELECT '()'::_sqlx_record_empty, $1")
|
|
.bind(RecordEmpty {})
|
|
.fetch_one(&mut conn)
|
|
.await?;
|
|
|
|
//
|
|
// Record of one element
|
|
//
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
struct Record1 {
|
|
_1: i64,
|
|
}
|
|
|
|
impl Type<Postgres> for Record1 {
|
|
fn type_info() -> PgTypeInfo {
|
|
PgTypeInfo::with_oid(OID_RECORD_1.load(Ordering::SeqCst))
|
|
}
|
|
}
|
|
|
|
impl Encode<Postgres> for Record1 {
|
|
fn encode(&self, buf: &mut Vec<u8>) {
|
|
PgRecordEncoder::new(buf).encode(self._1).finish();
|
|
}
|
|
}
|
|
|
|
impl<'de> Decode<'de, Postgres> for Record1 {
|
|
fn decode(value: Option<PgValue<'de>>) -> sqlx::Result<Self> {
|
|
let mut decoder = PgRecordDecoder::new(value)?;
|
|
|
|
let _1 = decoder.decode()?;
|
|
|
|
Ok(Record1 { _1 })
|
|
}
|
|
}
|
|
|
|
let rec: (Record1, Record1) = sqlx::query_as("SELECT '(324235)'::_sqlx_record_1, $1")
|
|
.bind(Record1 { _1: 324235 })
|
|
.fetch_one(&mut conn)
|
|
.await?;
|
|
|
|
assert_eq!(rec.0, rec.1);
|
|
|
|
Ok(())
|
|
}
|