1use std::str::FromStr;
4
5use prax_query::filter::FilterValue;
6use tokio_postgres::types::{IsNull, Kind, ToSql, Type};
7
8use crate::error::{PgError, PgResult};
9
10#[derive(Debug)]
18struct PgInt(i64);
19
20impl ToSql for PgInt {
21 fn to_sql(
22 &self,
23 ty: &Type,
24 out: &mut bytes::BytesMut,
25 ) -> Result<IsNull, Box<dyn std::error::Error + Sync + Send>> {
26 match *ty {
27 Type::INT2 => {
28 let v: i16 = self
29 .0
30 .try_into()
31 .map_err(|_| format!("value {} overflows INT2", self.0))?;
32 v.to_sql(ty, out)
33 }
34 Type::INT4 => {
35 let v: i32 = self
36 .0
37 .try_into()
38 .map_err(|_| format!("value {} overflows INT4", self.0))?;
39 v.to_sql(ty, out)
40 }
41 Type::INT8 => self.0.to_sql(ty, out),
42 _ => Err(format!("cannot bind integer to postgres type {ty:?}").into()),
43 }
44 }
45
46 fn accepts(ty: &Type) -> bool {
47 matches!(*ty, Type::INT2 | Type::INT4 | Type::INT8)
48 }
49
50 tokio_postgres::types::to_sql_checked!();
51}
52
53#[derive(Debug)]
60struct PgString(String);
61
62impl ToSql for PgString {
63 fn to_sql(
64 &self,
65 ty: &Type,
66 out: &mut bytes::BytesMut,
67 ) -> Result<IsNull, Box<dyn std::error::Error + Sync + Send>> {
68 match *ty {
69 Type::UUID => {
70 let parsed = ::uuid::Uuid::from_str(&self.0).map_err(|e| {
71 format!("FilterValue::String('{}') is not a valid UUID: {e}", self.0)
72 })?;
73 parsed.to_sql(ty, out)
74 }
75 Type::TIMESTAMPTZ => {
81 let parsed: ::chrono::DateTime<::chrono::Utc> =
82 ::chrono::DateTime::parse_from_rfc3339(&self.0)
83 .map_err(|e| {
84 format!(
85 "FilterValue::String('{}') is not a valid RFC3339 \
86 datetime for TIMESTAMPTZ: {e}",
87 self.0
88 )
89 })?
90 .with_timezone(&::chrono::Utc);
91 parsed.to_sql(ty, out)
92 }
93 Type::TIMESTAMP => {
94 let parsed =
95 ::chrono::NaiveDateTime::parse_from_str(&self.0, "%Y-%m-%dT%H:%M:%S%.f")
96 .map_err(|e| {
97 format!(
98 "FilterValue::String('{}') is not a valid \
99 ISO-8601 naive datetime for TIMESTAMP: {e}",
100 self.0
101 )
102 })?;
103 parsed.to_sql(ty, out)
104 }
105 Type::DATE => {
106 let parsed =
107 ::chrono::NaiveDate::parse_from_str(&self.0, "%Y-%m-%d").map_err(|e| {
108 format!(
109 "FilterValue::String('{}') is not a valid \
110 YYYY-MM-DD date for DATE: {e}",
111 self.0
112 )
113 })?;
114 parsed.to_sql(ty, out)
115 }
116 Type::TIME => {
117 let parsed =
118 ::chrono::NaiveTime::parse_from_str(&self.0, "%H:%M:%S%.f").map_err(|e| {
119 format!(
120 "FilterValue::String('{}') is not a valid \
121 HH:MM:SS time for TIME: {e}",
122 self.0
123 )
124 })?;
125 parsed.to_sql(ty, out)
126 }
127 _ => {
128 if matches!(ty.kind(), Kind::Enum(_)) {
133 out.extend_from_slice(self.0.as_bytes());
134 Ok(IsNull::No)
135 } else {
136 self.0.to_sql(ty, out)
137 }
138 }
139 }
140 }
141
142 fn accepts(_ty: &Type) -> bool {
143 true
144 }
145
146 tokio_postgres::types::to_sql_checked!();
147}
148
149#[derive(Debug)]
159struct PgNull;
160
161impl ToSql for PgNull {
162 fn to_sql(
163 &self,
164 _ty: &Type,
165 _out: &mut bytes::BytesMut,
166 ) -> Result<IsNull, Box<dyn std::error::Error + Sync + Send>> {
167 Ok(IsNull::Yes)
168 }
169
170 fn accepts(_ty: &Type) -> bool {
171 true
172 }
173
174 tokio_postgres::types::to_sql_checked!();
175}
176
177pub fn filter_value_to_sql(value: &FilterValue) -> PgResult<Box<dyn ToSql + Sync + Send>> {
179 match value {
180 FilterValue::Null => Ok(Box::new(PgNull)),
181 FilterValue::Bool(b) => Ok(Box::new(*b)),
182 FilterValue::Int(i) => Ok(Box::new(PgInt(*i))),
183 FilterValue::Float(f) => Ok(Box::new(*f)),
184 FilterValue::String(s) => Ok(Box::new(PgString(s.clone()))),
185 FilterValue::Json(j) => Ok(Box::new(j.clone())),
186 FilterValue::List(items) => {
187 let strings: Option<Vec<String>> = items
195 .iter()
196 .map(|item| match item {
197 FilterValue::String(s) => Some(s.clone()),
198 _ => None,
199 })
200 .collect();
201 match strings {
202 Some(strings) => Ok(Box::new(strings)),
203 None => Err(PgError::type_conversion(
204 "only lists of strings bind as Postgres arrays (TEXT[] and friends)",
205 )),
206 }
207 }
208 }
209}
210
211pub fn filter_values_to_params(
213 values: &[FilterValue],
214) -> PgResult<Vec<Box<dyn ToSql + Sync + Send>>> {
215 values.iter().map(filter_value_to_sql).collect()
216}
217
218pub mod pg_types {
220 use super::*;
221
222 pub fn rust_type_to_pg(rust_type: &str) -> Option<Type> {
224 match rust_type {
225 "i16" => Some(Type::INT2),
226 "i32" => Some(Type::INT4),
227 "i64" => Some(Type::INT8),
228 "f32" => Some(Type::FLOAT4),
229 "f64" => Some(Type::FLOAT8),
230 "bool" => Some(Type::BOOL),
231 "String" | "&str" => Some(Type::TEXT),
232 "Vec<u8>" | "&[u8]" => Some(Type::BYTEA),
233 "chrono::NaiveDate" => Some(Type::DATE),
234 "chrono::NaiveTime" => Some(Type::TIME),
235 "chrono::NaiveDateTime" => Some(Type::TIMESTAMP),
236 "chrono::DateTime<chrono::Utc>" => Some(Type::TIMESTAMPTZ),
237 "uuid::Uuid" => Some(Type::UUID),
238 "serde_json::Value" => Some(Type::JSONB),
239 _ => None,
240 }
241 }
242
243 pub fn pg_type_to_rust(pg_type: &Type) -> &'static str {
245 match *pg_type {
246 Type::BOOL => "bool",
247 Type::INT2 => "i16",
248 Type::INT4 => "i32",
249 Type::INT8 => "i64",
250 Type::FLOAT4 => "f32",
251 Type::FLOAT8 => "f64",
252 Type::TEXT | Type::VARCHAR | Type::CHAR | Type::NAME => "String",
253 Type::BYTEA => "Vec<u8>",
254 Type::DATE => "chrono::NaiveDate",
255 Type::TIME => "chrono::NaiveTime",
256 Type::TIMESTAMP => "chrono::NaiveDateTime",
257 Type::TIMESTAMPTZ => "chrono::DateTime<chrono::Utc>",
258 Type::UUID => "uuid::Uuid",
259 Type::JSON | Type::JSONB => "serde_json::Value",
260 _ => "unknown",
261 }
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn test_filter_value_to_sql() {
271 let result = filter_value_to_sql(&FilterValue::Int(42));
272 assert!(result.is_ok());
273
274 let result = filter_value_to_sql(&FilterValue::String("test".to_string()));
275 assert!(result.is_ok());
276
277 let result = filter_value_to_sql(&FilterValue::Bool(true));
278 assert!(result.is_ok());
279 }
280
281 #[test]
282 fn string_list_binds_as_a_text_array() {
283 let list = FilterValue::List(vec![
284 FilterValue::String("read".into()),
285 FilterValue::String("write".into()),
286 ]);
287 let param = filter_value_to_sql(&list).expect("string list binds");
288 let mut buf = bytes::BytesMut::new();
289 let encoded = param
290 .to_sql_checked(&Type::TEXT_ARRAY, &mut buf)
291 .expect("encodes as TEXT[]");
292 assert!(matches!(encoded, IsNull::No));
293 assert!(
294 param
295 .to_sql_checked(&Type::VARCHAR_ARRAY, &mut bytes::BytesMut::new())
296 .is_ok()
297 );
298 assert!(
300 param
301 .to_sql_checked(&Type::TEXT, &mut bytes::BytesMut::new())
302 .is_err()
303 );
304 }
305
306 #[test]
307 fn empty_list_binds_as_an_empty_text_array() {
308 let param = filter_value_to_sql(&FilterValue::List(vec![])).expect("empty list binds");
309 assert!(
310 param
311 .to_sql_checked(&Type::TEXT_ARRAY, &mut bytes::BytesMut::new())
312 .is_ok()
313 );
314 }
315
316 #[test]
317 fn non_string_list_is_still_rejected() {
318 let list = FilterValue::List(vec![FilterValue::Int(1), FilterValue::Int(2)]);
319 assert!(filter_value_to_sql(&list).is_err());
320 }
321
322 #[test]
323 fn test_pg_type_mapping() {
324 use pg_types::*;
325
326 assert_eq!(rust_type_to_pg("i32"), Some(Type::INT4));
327 assert_eq!(rust_type_to_pg("String"), Some(Type::TEXT));
328 assert_eq!(rust_type_to_pg("bool"), Some(Type::BOOL));
329
330 assert_eq!(pg_type_to_rust(&Type::INT4), "i32");
331 assert_eq!(pg_type_to_rust(&Type::TEXT), "String");
332 }
333}