Skip to content

Commit e8ecc5f

Browse files
committed
feat: implement Array and ArrayWithNullableItems wrapper types
1 parent 82b4dcd commit e8ecc5f

File tree

4 files changed

+286
-0
lines changed

4 files changed

+286
-0
lines changed

benzina/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ utoipa = ["dep:utoipa"]
5151
example-generated = ["typed-uuid"]
5252
dangerous-construction = ["typed-uuid"]
5353

54+
array = ["postgres"]
5455
json = ["postgres", "dep:serde_core", "dep:serde_json", "diesel/serde_json"]
5556
ctid = ["postgres", "diesel/i-implement-a-third-party-backend-and-opt-into-breaking-changes"]
5657

benzina/src/array.rs

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
use std::fmt::Debug;
2+
3+
use diesel::{
4+
deserialize::{FromSql, FromSqlRow},
5+
expression::{AppearsOnTable, Expression, SelectableExpression},
6+
pg::{Pg, PgValue},
7+
query_builder::{AstPass, QueryFragment, QueryId},
8+
result::QueryResult,
9+
serialize::ToSql,
10+
sql_types::{self, BigInt, Bool, Double, Float, Integer, Nullable, SmallInt, Text},
11+
};
12+
13+
use crate::{U15, U31, U63, error::InvalidArray};
14+
15+
/// A diesel [`Array`] serialization and deserialization wrapper __without__ NULL items
16+
///
17+
/// Since postgres ignores the array dimension (if specified), diesel implements
18+
/// [`FromSql`] for `Vec<T>` and [`ToSql`] for `Vec<T>`/`&[T]`.
19+
/// In addition postgres also considers array items as always nullable.
20+
/// This makes it hard to deal with real arrays that have a predetermined length
21+
/// and an homogeneous nullability.
22+
/// This type checks at runtime the array length and the __non__ nullability of its items,
23+
/// therefore to be completely safe, you should also add some `CHECK` constraints to your array fields.
24+
///
25+
/// This type is not intended to be used directly in the model but rather to be
26+
/// used with diesel [`serialize_as`] and [`deserialize_as`].
27+
///
28+
/// ```
29+
/// use benzina::{Array, U31};
30+
/// use diesel::{Insertable, Queryable};
31+
///
32+
/// #[derive(Debug, Queryable)]
33+
/// #[diesel(table_name = users, check_for_backend(diesel::pg::Pg))]
34+
/// struct User {
35+
/// id: U31,
36+
/// first_name: String,
37+
/// last_name: String,
38+
/// #[diesel(deserialize_as = Array<bool, 5>)]
39+
/// flags: UserFlags,
40+
/// }
41+
///
42+
/// #[derive(Debug, Insertable)]
43+
/// #[diesel(table_name = users)]
44+
/// struct NewUser {
45+
/// first_name: String,
46+
/// last_name: String,
47+
/// #[diesel(serialize_as = Array<bool, 5>)]
48+
/// flags: UserFlags,
49+
/// }
50+
///
51+
/// #[derive(Debug)]
52+
/// struct UserFlags([bool; 5]);
53+
///
54+
/// // needed by deserialize_as
55+
/// impl From<benzina::Array<bool, 5>> for UserFlags {
56+
/// fn from(value: benzina::Array<bool, 5>) -> Self {
57+
/// Self(value.into_inner())
58+
/// }
59+
/// }
60+
///
61+
/// // needed by serialize_as
62+
/// impl From<UserFlags> for benzina::Array<bool, 5> {
63+
/// fn from(value: UserFlags) -> Self {
64+
/// Self::new(value.0)
65+
/// }
66+
/// }
67+
///
68+
/// diesel::table! {
69+
/// users (id) {
70+
/// id -> Int4,
71+
/// first_name -> Text,
72+
/// last_name -> Text,
73+
/// flags -> Array<Nullable<Bool>>,
74+
/// }
75+
/// }
76+
/// ```
77+
///
78+
/// [`Array`]: diesel::sql_types::Array
79+
/// [`serialize_as`]: diesel::prelude::Insertable#optional-field-attributes
80+
/// [`deserialize_as`]: diesel::prelude::Queryable#deserialize_as-attribute
81+
#[derive(Debug, FromSqlRow)]
82+
pub struct Array<T, const N: usize>([T; N]);
83+
impl<T, const N: usize> Array<T, N> {
84+
pub fn new(values: [T; N]) -> Self {
85+
Self(values)
86+
}
87+
88+
pub fn get(&self) -> &[T; N] {
89+
&self.0
90+
}
91+
92+
pub fn into_inner(self) -> [T; N] {
93+
self.0
94+
}
95+
}
96+
97+
/// A diesel [`Array`](diesel::sql_types::Array) serialization and deserialization wrapper __with__ NULL items
98+
///
99+
/// This type works exactly as benzina [`Array`](crate::Array), with the only exeception that
100+
/// it does not require __non__ nullable items.
101+
#[derive(Debug, FromSqlRow)]
102+
pub struct ArrayWithNullableItems<T, const N: usize>([Option<T>; N]);
103+
impl<T, const N: usize> ArrayWithNullableItems<T, N> {
104+
pub fn new(values: [Option<T>; N]) -> Self {
105+
Self(values)
106+
}
107+
108+
pub fn get(&self) -> &[Option<T>; N] {
109+
&self.0
110+
}
111+
112+
pub fn into_inner(self) -> [Option<T>; N] {
113+
self.0
114+
}
115+
}
116+
117+
macro_rules! impl_array {
118+
(
119+
$(
120+
$rust_type:tt $(< $generic:ident >)? => $diesel_type:ident
121+
),*
122+
) => {
123+
$(
124+
impl<$($generic,)? const N: usize> Expression for Array<$rust_type$(<$generic>)?, N> {
125+
type SqlType = sql_types::Array<Nullable<$diesel_type>>;
126+
}
127+
128+
impl<$($generic,)? const N: usize> QueryId for Array<$rust_type$(<$generic>)?, N> {
129+
type QueryId = <sql_types::Array<Nullable<$diesel_type>> as QueryId>::QueryId;
130+
131+
const HAS_STATIC_QUERY_ID: bool = <sql_types::Array<Nullable<$diesel_type>> as QueryId>::HAS_STATIC_QUERY_ID;
132+
}
133+
134+
impl<$($generic: Debug + std::clone::Clone,)? const N: usize> QueryFragment<Pg> for Array<$rust_type$(<$generic>)?, N>
135+
{
136+
fn walk_ast<'b>(&'b self, mut pass: AstPass<'_, 'b, Pg>) -> QueryResult<()> {
137+
pass.push_bind_param(self)?;
138+
Ok(())
139+
}
140+
}
141+
142+
impl<__QS, $($generic,)? const N: usize> AppearsOnTable<__QS> for Array<$rust_type$(<$generic>)?, N> {}
143+
144+
impl<__QS, $($generic,)? const N: usize> SelectableExpression<__QS> for Array<$rust_type$(<$generic>)?, N> {}
145+
146+
impl<$($generic: Debug + std::clone::Clone,)? const N: usize> ToSql<sql_types::Array<Nullable<$diesel_type>>, Pg> for Array<$rust_type$(<$generic>)?, N>
147+
{
148+
fn to_sql<'b>(
149+
&'b self,
150+
out: &mut diesel::serialize::Output<'b, '_, Pg>,
151+
) -> diesel::serialize::Result {
152+
<[$rust_type $(< $generic >)?] as ToSql<sql_types::Array<$diesel_type>, Pg>>::to_sql(&self.0.as_slice(), out)
153+
}
154+
}
155+
156+
impl<$($generic: Debug,)? const N: usize> FromSql<sql_types::Array<Nullable<$diesel_type>>, Pg> for Array<$rust_type$(< $generic >)?, N>
157+
{
158+
fn from_sql(bytes: PgValue<'_>) -> diesel::deserialize::Result<Self> {
159+
let raw = <Vec<Option<$rust_type $(< $generic >)?>> as FromSql<sql_types::Array<Nullable<$diesel_type>>, Pg>>::from_sql(bytes)?;
160+
161+
let res: [$rust_type $(< $generic >)?; N] = raw
162+
.into_iter()
163+
.collect::<Option<Vec<$rust_type $(< $generic >)?>>>()
164+
.ok_or(diesel::result::Error::DeserializationError(Box::new(
165+
InvalidArray::UnexpectedNullValue,
166+
)))?
167+
.try_into()
168+
.map_err(|_| {
169+
diesel::result::Error::DeserializationError(Box::new(
170+
InvalidArray::UnexpectedLength,
171+
))
172+
})?;
173+
174+
Ok(Self(res))
175+
}
176+
}
177+
)*
178+
}
179+
}
180+
181+
macro_rules! impl_array_with_nullable_items {
182+
(
183+
$(
184+
$rust_type:tt $(< $generic:ident >)? => $diesel_type:ident
185+
),*
186+
) => {
187+
$(
188+
impl<$($generic,)? const N: usize> Expression for ArrayWithNullableItems<$rust_type$(<$generic>)?, N> {
189+
type SqlType = sql_types::Array<Nullable<$diesel_type>>;
190+
}
191+
192+
impl<$($generic,)? const N: usize> QueryId for ArrayWithNullableItems<$rust_type$(<$generic>)?, N> {
193+
type QueryId = <sql_types::Array<Nullable<$diesel_type>> as QueryId>::QueryId;
194+
195+
const HAS_STATIC_QUERY_ID: bool = <sql_types::Array<Nullable<$diesel_type>> as QueryId>::HAS_STATIC_QUERY_ID;
196+
}
197+
198+
impl<$($generic: Debug + std::clone::Clone,)? const N: usize> QueryFragment<Pg> for ArrayWithNullableItems<$rust_type$(<$generic>)?, N>
199+
{
200+
fn walk_ast<'b>(&'b self, mut pass: AstPass<'_, 'b, Pg>) -> QueryResult<()> {
201+
pass.push_bind_param(self)?;
202+
Ok(())
203+
}
204+
}
205+
206+
impl<__QS, $($generic,)? const N: usize> AppearsOnTable<__QS> for ArrayWithNullableItems<$rust_type$(<$generic>)?, N> {}
207+
impl<__QS, $($generic,)? const N: usize> SelectableExpression<__QS> for ArrayWithNullableItems<$rust_type$(<$generic>)?, N> {}
208+
209+
210+
impl<$($generic: Debug + std::clone::Clone,)? const N: usize> ToSql<sql_types::Array<Nullable<$diesel_type>>, Pg> for ArrayWithNullableItems<$rust_type$(<$generic>)?, N>
211+
{
212+
fn to_sql<'b>(
213+
&'b self,
214+
out: &mut diesel::serialize::Output<'b, '_, Pg>,
215+
) -> diesel::serialize::Result {
216+
<[Option<$rust_type$(< $generic >)?>] as ToSql<sql_types::Array<Nullable<$diesel_type>>, Pg>>::to_sql(self.0.as_slice(), out)
217+
}
218+
}
219+
220+
impl<$($generic: Debug,)? const N: usize> FromSql<sql_types::Array<Nullable<$diesel_type>>, Pg> for ArrayWithNullableItems<$rust_type$(< $generic >)?, N>
221+
{
222+
fn from_sql(bytes: PgValue<'_>) -> diesel::deserialize::Result<Self> {
223+
let raw = <Vec<Option<$rust_type$(< $generic >)?>> as FromSql<sql_types::Array<Nullable<$diesel_type>>, Pg>>::from_sql(bytes)?;
224+
225+
let res: [Option<$rust_type $(< $generic >)?>; N] = raw
226+
.try_into()
227+
.map_err(|_| {
228+
diesel::result::Error::DeserializationError(Box::new(
229+
InvalidArray::UnexpectedLength,
230+
))
231+
})?;
232+
233+
Ok(Self(res))
234+
}
235+
}
236+
)*
237+
};
238+
}
239+
240+
impl_array! {
241+
U15 => SmallInt,
242+
U31 => Integer,
243+
U63 => BigInt,
244+
i16 => SmallInt,
245+
i32 => Integer,
246+
i64 => BigInt,
247+
f32 => Float,
248+
f64 => Double,
249+
bool => Bool,
250+
String => Text
251+
}
252+
253+
impl_array_with_nullable_items! {
254+
U15 => SmallInt,
255+
U31 => Integer,
256+
U63 => BigInt,
257+
i16 => SmallInt,
258+
i32 => Integer,
259+
i64 => BigInt,
260+
f32 => Float,
261+
f64 => Double,
262+
bool => Bool,
263+
String => Text
264+
}

benzina/src/error.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,20 @@ impl Error for ParseIntError {
3838
}
3939
}
4040
}
41+
42+
#[derive(Debug, Clone)]
43+
pub enum InvalidArray {
44+
UnexpectedLength,
45+
UnexpectedNullValue,
46+
}
47+
48+
impl Display for InvalidArray {
49+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50+
f.write_str(match self {
51+
Self::UnexpectedLength => "mismatched array length",
52+
Self::UnexpectedNullValue => "the array contains an unexpected null value",
53+
})
54+
}
55+
}
56+
57+
impl Error for InvalidArray {}

benzina/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
22

3+
#[cfg(feature = "array")]
4+
pub use self::array::{Array, ArrayWithNullableItems};
35
#[cfg(feature = "ctid")]
46
pub use self::ctid::{Ctid, ctid};
57
#[cfg(feature = "postgres")]
@@ -15,6 +17,8 @@ pub use benzina_derive::{Enum, join};
1517

1618
#[doc(hidden)]
1719
pub mod __private;
20+
#[cfg(feature = "array")]
21+
mod array;
1822
#[cfg(feature = "ctid")]
1923
mod ctid;
2024
#[cfg(feature = "postgres")]

0 commit comments

Comments
 (0)