feat(postgres): add support for SQL NULL to Option<_> and sqlx::Null

This commit is contained in:
Ryan Leckey
2021-04-16 16:10:02 -07:00
parent 23fb65dc36
commit e7664d8d19
7 changed files with 120 additions and 28 deletions

View File

@@ -6,6 +6,22 @@ use crate::{decode, encode, Database, Decode, Encode, RawValue, Type};
#[derive(Debug)]
pub struct Null;
impl<Db: Database> Encode<Db> for Null {
fn encode(
&self,
_: &<Db as Database>::TypeInfo,
_: &mut <Db as HasOutput<'_>>::Output,
) -> encode::Result {
Ok(encode::IsNull::Yes)
}
}
impl<'r, Db: Database> Decode<'r, Db> for Null {
fn decode(_: <Db as HasRawValue<'r>>::RawValue) -> decode::Result<Self> {
Ok(Self)
}
}
impl<Db: Database, T: Type<Db>> Type<Db> for Option<T>
where
Null: Type<Db>,

View File

@@ -47,7 +47,9 @@ impl Serialize<'_> for Execute<'_, '_> {
for param in self.parameters {
if let Some(argument) = args.next() {
if let IsNull::Yes = argument.encode(param, &mut out)? {
// no data *should* have been written to the buffer if we were told the expression is NULL
debug_assert!(out.buffer().len() != prev_len);
out.null();
}

View File

@@ -1,7 +1,6 @@
use sqlx_core::database::{HasOutput, HasRawValue};
use sqlx_core::{decode, encode, Database, Decode, Encode, Null, Type};
use sqlx_core::{Null, Type};
use crate::{MySql, MySqlOutput, MySqlRawValue, MySqlTypeId, MySqlTypeInfo};
use crate::{MySql, MySqlTypeId};
impl Type<MySql> for Null {
fn type_id() -> MySqlTypeId
@@ -11,15 +10,3 @@ impl Type<MySql> for Null {
MySqlTypeId::NULL
}
}
impl Encode<MySql> for Null {
fn encode(&self, _: &MySqlTypeInfo, _: &mut MySqlOutput<'_>) -> encode::Result {
Ok(encode::IsNull::Yes)
}
}
impl<'r> Decode<'r, MySql> for Null {
fn decode(_: MySqlRawValue<'r>) -> decode::Result<Self> {
Ok(Self)
}
}

View File

@@ -6,6 +6,7 @@ use sqlx_core::Result;
use crate::io::PgWriteExt;
use crate::protocol::frontend::{PortalRef, StatementRef};
use crate::{PgArguments, PgOutput, PgRawValueFormat, PgTypeInfo};
use sqlx_core::encode::IsNull;
pub(crate) struct Bind<'a> {
pub(crate) portal: PortalRef,
@@ -50,16 +51,28 @@ impl Serialize<'_> for Bind<'_> {
let offset = out.buffer().len();
out.buffer().extend_from_slice(&[0; 4]);
let len = if let Some(argument) = args.next() {
argument.encode(param, &mut out)?;
// prefixed length does not include the length in the length
// unlike the regular "prefixed length" bytes protocol type
(out.buffer().len() - offset - 4) as i32
} else {
let prev_len = out.buffer().len();
let null = args
.next()
.map(|argument| argument.encode(param, &mut out))
.transpose()?
// if we run out of values, start sending NULL for
.unwrap_or(IsNull::Yes);
let len = match null {
// NULL is encoded as a -1 for the length
-1_i32
IsNull::Yes => {
// no data *should* have been written to the buffer if we were told the expression is NULL
debug_assert_eq!(prev_len, out.buffer().len());
-1_i32
}
IsNull::No => {
// prefixed length does not include the length in the length
// unlike the regular "prefixed length" bytes protocol type
(out.buffer().len() - offset - 4) as i32
}
};
// write the len to the beginning of the value

View File

@@ -1,4 +1,5 @@
mod bool;
mod int;
mod null;
// https://www.postgresql.org/docs/current/datatype.html

View File

@@ -44,6 +44,30 @@ fn ensure_not_too_large_or_too_small(value: i128, ty: &PgTypeInfo) -> Result<(),
Ok(())
}
fn ensure_not_too_large(value: u128, ty: &PgTypeInfo) -> Result<(), encode::Error> {
let max: u128 = match ty.id() {
PgTypeId::SMALLINT => i16::MAX as _,
PgTypeId::INTEGER => i32::MAX as _,
PgTypeId::BIGINT => i64::MAX as _,
_ => {
// for non-integer types, ignore the check
// if we got this far its because someone asked for and `_unchecked` bind
return Ok(());
}
};
if value > max {
return Err(encode::Error::msg(format!(
"number `{}` too large to fit in SQL type `{}`",
value,
ty.name()
)));
}
Ok(())
}
fn decode_int<T>(value: &PgRawValue<'_>) -> decode::Result<T>
where
T: TryFrom<i64> + TryFrom<u64> + FromStr,
@@ -97,8 +121,38 @@ impl_type_int! { i32 => INTEGER }
impl_type_int! { i64 => BIGINT }
impl_type_int! { i128 => BIGINT }
impl_type_int! { u8 => SMALLINT }
impl_type_int! { u16 => SMALLINT }
impl_type_int! { u32 => INTEGER }
impl_type_int! { u64 => BIGINT }
impl_type_int! { u128 => BIGINT }
macro_rules! impl_type_uint {
($ty:ty $(: $real:ty)? => $sql:ident) => {
impl Type<Postgres> for $ty {
fn type_id() -> PgTypeId {
PgTypeId::$sql
}
fn compatible(ty: &PgTypeInfo) -> bool {
ty.id().is_integer()
}
}
impl Encode<Postgres> for $ty {
fn encode(&self, ty: &PgTypeInfo, out: &mut PgOutput<'_>) -> encode::Result {
ensure_not_too_large((*self $(as $real)?).into(), ty)?;
out.buffer().extend_from_slice(&self.to_be_bytes());
Ok(encode::IsNull::No)
}
}
impl<'r> Decode<'r, Postgres> for $ty {
fn decode(value: PgRawValue<'r>) -> decode::Result<Self> {
decode_int(&value)
}
}
};
}
impl_type_uint! { u8 => SMALLINT }
impl_type_uint! { u16 => SMALLINT }
impl_type_uint! { u32 => INTEGER }
impl_type_uint! { u64 => BIGINT }
impl_type_uint! { u128 => BIGINT }

View File

@@ -0,0 +1,19 @@
use sqlx_core::{Null, Type};
use crate::{PgTypeId, PgTypeInfo, Postgres};
impl Type<Postgres> for Null {
fn type_id() -> PgTypeId
where
Self: Sized,
{
PgTypeId::Oid(0)
}
fn compatible(_: &PgTypeInfo) -> bool
where
Self: Sized,
{
true
}
}