Skip to main content

tnid/
sqlx_impl.rs

1//! `sqlx` integration for TNID types.
2//!
3//! Enabled by one of:
4//! - `sqlx-postgres`
5//! - `sqlx-mysql`
6//! - `sqlx-sqlite`
7//!
8//! ## Representation
9//!
10//! TNIDs are encoded/decoded as **UUID bytes** by default (16 bytes, big-endian).
11//! This matches typical `UUID`/`BINARY(16)`/`BLOB` storage and avoids inflating storage with text.
12
13use crate::{DynamicTnid, Tnid, TnidName};
14
15fn u128_to_be_bytes(id: u128) -> [u8; 16] {
16    id.to_be_bytes()
17}
18
19fn be_bytes_to_u128(bytes: &[u8]) -> Result<u128, sqlx::error::BoxDynError> {
20    if bytes.len() != 16 {
21        return Err(Box::new(std::io::Error::new(
22            std::io::ErrorKind::InvalidData,
23            format!("expected 16 bytes, got {}", bytes.len()),
24        )));
25    }
26
27    let mut arr = [0_u8; 16];
28    arr.copy_from_slice(bytes);
29    Ok(u128::from_be_bytes(arr))
30}
31
32#[cfg(feature = "sqlx-postgres")]
33mod postgres {
34    use super::*;
35    use sqlx::encode::{Encode, IsNull};
36    use sqlx::types::Type;
37    use sqlx::Postgres;
38
39    impl<Name: TnidName> Type<Postgres> for Tnid<Name> {
40        fn type_info() -> sqlx::postgres::PgTypeInfo {
41            // Use `with_name` because `PgTypeInfo::UUID` is not public in sqlx.
42            sqlx::postgres::PgTypeInfo::with_name("UUID")
43        }
44    }
45
46    impl<Name: TnidName> sqlx::postgres::PgHasArrayType for Tnid<Name> {
47        fn array_type_info() -> sqlx::postgres::PgTypeInfo {
48            sqlx::postgres::PgTypeInfo::array_of("UUID")
49        }
50    }
51
52    impl<'q, Name: TnidName> Encode<'q, Postgres> for Tnid<Name> {
53        fn encode_by_ref(
54            &self,
55            buf: &mut sqlx::postgres::PgArgumentBuffer,
56        ) -> Result<IsNull, sqlx::error::BoxDynError> {
57            let bytes = u128_to_be_bytes(self.as_u128());
58            buf.extend_from_slice(&bytes);
59            Ok(IsNull::No)
60        }
61    }
62
63    impl<'r, Name: TnidName> sqlx::decode::Decode<'r, Postgres> for Tnid<Name> {
64        fn decode(value: sqlx::postgres::PgValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
65            let id = match value.format() {
66                sqlx::postgres::PgValueFormat::Binary => be_bytes_to_u128(value.as_bytes()?)?,
67                sqlx::postgres::PgValueFormat::Text => {
68                    let s = value.as_str()?;
69                    crate::UuidLike::parse_uuid_string(s)
70                        .map_err(|e| Box::new(e) as sqlx::error::BoxDynError)?
71                        .as_u128()
72                }
73            };
74
75            Tnid::<Name>::from_u128(id).map_err(|e| Box::new(e) as sqlx::error::BoxDynError)
76        }
77    }
78
79    impl Type<Postgres> for DynamicTnid {
80        fn type_info() -> sqlx::postgres::PgTypeInfo {
81            sqlx::postgres::PgTypeInfo::with_name("UUID")
82        }
83    }
84
85    impl sqlx::postgres::PgHasArrayType for DynamicTnid {
86        fn array_type_info() -> sqlx::postgres::PgTypeInfo {
87            sqlx::postgres::PgTypeInfo::array_of("UUID")
88        }
89    }
90
91    impl<'q> Encode<'q, Postgres> for DynamicTnid {
92        fn encode_by_ref(
93            &self,
94            buf: &mut sqlx::postgres::PgArgumentBuffer,
95        ) -> Result<IsNull, sqlx::error::BoxDynError> {
96            let bytes = u128_to_be_bytes(self.as_u128());
97            buf.extend_from_slice(&bytes);
98            Ok(IsNull::No)
99        }
100    }
101
102    impl<'r> sqlx::decode::Decode<'r, Postgres> for DynamicTnid {
103        fn decode(value: sqlx::postgres::PgValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
104            let id = match value.format() {
105                sqlx::postgres::PgValueFormat::Binary => be_bytes_to_u128(value.as_bytes()?)?,
106                sqlx::postgres::PgValueFormat::Text => {
107                    let s = value.as_str()?;
108                    crate::UuidLike::parse_uuid_string(s)
109                        .map_err(|e| Box::new(e) as sqlx::error::BoxDynError)?
110                        .as_u128()
111                }
112            };
113
114            DynamicTnid::from_u128(id).map_err(|e| Box::new(e) as sqlx::error::BoxDynError)
115        }
116    }
117}
118
119#[cfg(feature = "sqlx-mysql")]
120mod mysql {
121    use super::*;
122    use sqlx::decode::Decode;
123    use sqlx::encode::{Encode, IsNull};
124    use sqlx::types::Type;
125    use sqlx::MySql;
126
127    impl<Name: TnidName> Type<MySql> for Tnid<Name> {
128        fn type_info() -> sqlx::mysql::MySqlTypeInfo {
129            <&[u8] as Type<MySql>>::type_info()
130        }
131
132        fn compatible(ty: &sqlx::mysql::MySqlTypeInfo) -> bool {
133            <&[u8] as Type<MySql>>::compatible(ty)
134        }
135    }
136
137    impl<'q, Name: TnidName> Encode<'q, MySql> for Tnid<Name> {
138        fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, sqlx::error::BoxDynError> {
139            let bytes = u128_to_be_bytes(self.as_u128());
140            <&[u8] as Encode<MySql>>::encode_by_ref(&bytes.as_slice(), buf)
141        }
142    }
143
144    impl<'r, Name: TnidName> Decode<'r, MySql> for Tnid<Name> {
145        fn decode(value: sqlx::mysql::MySqlValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
146            let bytes = <&[u8] as Decode<MySql>>::decode(value)?;
147            let id = be_bytes_to_u128(bytes)?;
148            Tnid::<Name>::from_u128(id).map_err(|e| Box::new(e) as sqlx::error::BoxDynError)
149        }
150    }
151
152    impl Type<MySql> for DynamicTnid {
153        fn type_info() -> sqlx::mysql::MySqlTypeInfo {
154            <&[u8] as Type<MySql>>::type_info()
155        }
156
157        fn compatible(ty: &sqlx::mysql::MySqlTypeInfo) -> bool {
158            <&[u8] as Type<MySql>>::compatible(ty)
159        }
160    }
161
162    impl<'q> Encode<'q, MySql> for DynamicTnid {
163        fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, sqlx::error::BoxDynError> {
164            let bytes = u128_to_be_bytes(self.as_u128());
165            <&[u8] as Encode<MySql>>::encode_by_ref(&bytes.as_slice(), buf)
166        }
167    }
168
169    impl<'r> Decode<'r, MySql> for DynamicTnid {
170        fn decode(value: sqlx::mysql::MySqlValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
171            let bytes = <&[u8] as Decode<MySql>>::decode(value)?;
172            let id = be_bytes_to_u128(bytes)?;
173            DynamicTnid::from_u128(id).map_err(|e| Box::new(e) as sqlx::error::BoxDynError)
174        }
175    }
176}
177
178#[cfg(feature = "sqlx-sqlite")]
179mod sqlite {
180    use super::*;
181    use sqlx::decode::Decode;
182    use sqlx::encode::{Encode, IsNull};
183    use sqlx::types::Type;
184    use sqlx::Sqlite;
185    use sqlx::TypeInfo;
186    use sqlx::ValueRef;
187    use std::borrow::Cow;
188
189    impl<Name: TnidName> Type<Sqlite> for Tnid<Name> {
190        fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
191            <&[u8] as Type<Sqlite>>::type_info()
192        }
193
194        fn compatible(ty: &sqlx::sqlite::SqliteTypeInfo) -> bool {
195            <&[u8] as Type<Sqlite>>::compatible(ty)
196        }
197    }
198
199    impl<'q, Name: TnidName> Encode<'q, Sqlite> for Tnid<Name> {
200        fn encode_by_ref(
201            &self,
202            args: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>,
203        ) -> Result<IsNull, sqlx::error::BoxDynError> {
204            let bytes = u128_to_be_bytes(self.as_u128());
205            args.push(sqlx::sqlite::SqliteArgumentValue::Blob(Cow::Owned(
206                bytes.to_vec(),
207            )));
208            Ok(IsNull::No)
209        }
210    }
211
212    impl<'r, Name: TnidName> Decode<'r, Sqlite> for Tnid<Name> {
213        fn decode(
214            value: sqlx::sqlite::SqliteValueRef<'r>,
215        ) -> Result<Self, sqlx::error::BoxDynError> {
216            let ty_name = value.type_info().name().to_ascii_uppercase();
217
218            let id = if ty_name == "TEXT" {
219                let s = <&str as Decode<Sqlite>>::decode(value)?;
220                if s.as_bytes().contains(&b'.') {
221                    Tnid::<Name>::parse_tnid_string(s)
222                        .map_err(|e| Box::new(e) as sqlx::error::BoxDynError)?
223                        .as_u128()
224                } else {
225                    Tnid::<Name>::parse_uuid_string(s)
226                        .map_err(|e| Box::new(e) as sqlx::error::BoxDynError)?
227                        .as_u128()
228                }
229            } else {
230                let bytes = <&[u8] as Decode<Sqlite>>::decode(value)?;
231                be_bytes_to_u128(bytes)?
232            };
233
234            Tnid::<Name>::from_u128(id).map_err(|e| Box::new(e) as sqlx::error::BoxDynError)
235        }
236    }
237
238    impl Type<Sqlite> for DynamicTnid {
239        fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
240            <&[u8] as Type<Sqlite>>::type_info()
241        }
242
243        fn compatible(ty: &sqlx::sqlite::SqliteTypeInfo) -> bool {
244            <&[u8] as Type<Sqlite>>::compatible(ty)
245        }
246    }
247
248    impl<'q> Encode<'q, Sqlite> for DynamicTnid {
249        fn encode_by_ref(
250            &self,
251            args: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>,
252        ) -> Result<IsNull, sqlx::error::BoxDynError> {
253            let bytes = u128_to_be_bytes(self.as_u128());
254            args.push(sqlx::sqlite::SqliteArgumentValue::Blob(Cow::Owned(
255                bytes.to_vec(),
256            )));
257            Ok(IsNull::No)
258        }
259    }
260
261    impl<'r> Decode<'r, Sqlite> for DynamicTnid {
262        fn decode(
263            value: sqlx::sqlite::SqliteValueRef<'r>,
264        ) -> Result<Self, sqlx::error::BoxDynError> {
265            let ty_name = value.type_info().name().to_ascii_uppercase();
266
267            let id = if ty_name == "TEXT" {
268                let s = <&str as Decode<Sqlite>>::decode(value)?;
269                if s.as_bytes().contains(&b'.') {
270                    DynamicTnid::parse_tnid_string(s)
271                        .map_err(|e| Box::new(e) as sqlx::error::BoxDynError)?
272                        .as_u128()
273                } else {
274                    crate::UuidLike::parse_uuid_string(s)
275                        .map_err(|e| Box::new(e) as sqlx::error::BoxDynError)?
276                        .as_u128()
277                }
278            } else {
279                let bytes = <&[u8] as Decode<Sqlite>>::decode(value)?;
280                be_bytes_to_u128(bytes)?
281            };
282
283            DynamicTnid::from_u128(id).map_err(|e| Box::new(e) as sqlx::error::BoxDynError)
284        }
285    }
286}