1use crate::numeric::PgNumeric;
6use crate::row_json::base_type;
7use bytes::BytesMut;
8use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
9use cufflink_types::db::{DbError, DbErrorFields, DbErrorKind, Param};
10use serde_json::Value;
11use std::error::Error;
12use std::str::FromStr;
13use tokio_postgres::types::{to_sql_checked, IsNull, Kind, ToSql, Type};
14use uuid::Uuid;
15
16#[derive(Debug, Clone, PartialEq)]
17pub enum Bound {
18 Null,
19 Bool(bool),
20 I16(i16),
21 I32(i32),
22 I64(i64),
23 F32(f32),
24 F64(f64),
25 Text(String),
27 Uuid(Uuid),
28 Timestamptz(DateTime<Utc>),
29 Timestamp(NaiveDateTime),
30 Date(NaiveDate),
31 Time(NaiveTime),
32 Json(Value),
33 Numeric(PgNumeric),
34 Bytes(Vec<u8>),
35 TextArray(Vec<Option<String>>),
36 UuidArray(Vec<Option<Uuid>>),
37 I16Array(Vec<Option<i16>>),
38 I32Array(Vec<Option<i32>>),
39 I64Array(Vec<Option<i64>>),
40 BoolArray(Vec<Option<bool>>),
41 F64Array(Vec<Option<f64>>),
42 NumericArray(Vec<Option<PgNumeric>>),
43 JsonArray(Vec<Option<Value>>),
44}
45
46impl ToSql for Bound {
50 fn to_sql(
51 &self,
52 ty: &Type,
53 out: &mut BytesMut,
54 ) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
55 let ty = base_type(ty);
56 match self {
57 Bound::Null => Ok(IsNull::Yes),
58 Bound::Bool(v) => v.to_sql(ty, out),
59 Bound::I16(v) => v.to_sql(ty, out),
60 Bound::I32(v) => v.to_sql(ty, out),
61 Bound::I64(v) => v.to_sql(ty, out),
62 Bound::F32(v) => v.to_sql(ty, out),
63 Bound::F64(v) => v.to_sql(ty, out),
64 Bound::Text(v) => {
65 out.extend_from_slice(v.as_bytes());
66 Ok(IsNull::No)
67 }
68 Bound::Uuid(v) => v.to_sql(ty, out),
69 Bound::Timestamptz(v) => v.to_sql(ty, out),
70 Bound::Timestamp(v) => v.to_sql(ty, out),
71 Bound::Date(v) => v.to_sql(ty, out),
72 Bound::Time(v) => v.to_sql(ty, out),
73 Bound::Json(v) => v.to_sql(ty, out),
74 Bound::Numeric(v) => v.to_sql(ty, out),
75 Bound::Bytes(v) => v.to_sql(ty, out),
76 Bound::TextArray(v) => array(v, ty, out),
77 Bound::UuidArray(v) => array(v, ty, out),
78 Bound::I16Array(v) => array(v, ty, out),
79 Bound::I32Array(v) => array(v, ty, out),
80 Bound::I64Array(v) => array(v, ty, out),
81 Bound::BoolArray(v) => array(v, ty, out),
82 Bound::F64Array(v) => array(v, ty, out),
83 Bound::NumericArray(v) => array(v, ty, out),
84 Bound::JsonArray(v) => array(v, ty, out),
85 }
86 }
87
88 fn accepts(_: &Type) -> bool {
89 true
90 }
91
92 to_sql_checked!();
93}
94
95fn array<T: ToSql>(
96 values: &[Option<T>],
97 ty: &Type,
98 out: &mut BytesMut,
99) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
100 if !matches!(ty.kind(), Kind::Array(_)) {
101 return Err(format!("an array cannot be sent as {}", ty.name()).into());
102 }
103 values
104 .iter()
105 .map(|value| value.as_ref().map(OnBase))
106 .collect::<Vec<_>>()
107 .to_sql(ty, out)
108}
109
110#[derive(Debug)]
112struct OnBase<'a, T>(&'a T);
113
114impl<T: ToSql> ToSql for OnBase<'_, T> {
115 fn to_sql(
116 &self,
117 ty: &Type,
118 out: &mut BytesMut,
119 ) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
120 self.0.to_sql(base_type(ty), out)
121 }
122
123 fn accepts(_: &Type) -> bool {
124 true
125 }
126
127 to_sql_checked!();
128}
129
130pub fn bind(types: &[Type], params: &[Param]) -> Result<Vec<Bound>, DbError> {
132 if types.len() != params.len() {
133 return Err(DbError::new(
134 DbErrorKind::InvalidParameter,
135 format!(
136 "the statement takes {} parameter(s), {} were given",
137 types.len(),
138 params.len()
139 ),
140 ));
141 }
142 params
143 .iter()
144 .zip(types)
145 .enumerate()
146 .map(|(i, (param, ty))| coerce(i + 1, param, ty))
147 .collect()
148}
149
150enum Refusal {
152 Mismatch(Option<String>),
153 Unsupported,
154}
155
156type Coerced = Result<Bound, Refusal>;
157
158fn mismatch() -> Refusal {
159 Refusal::Mismatch(None)
160}
161
162fn invalid(reason: impl ToString) -> Refusal {
163 Refusal::Mismatch(Some(reason.to_string()))
164}
165
166pub fn coerce(index: usize, param: &Param, ty: &Type) -> Result<Bound, DbError> {
168 coerce_to(param, ty).map_err(|refusal| match refusal {
169 Refusal::Mismatch(reason) => DbError::new(
170 DbErrorKind::InvalidParameter,
171 format!(
172 "parameter ${index}: expected {}, got {}{}",
173 ty.name(),
174 describe(param),
175 reason.map(|r| format!(" ({r})")).unwrap_or_default()
176 ),
177 ),
178 Refusal::Unsupported => DbErrorFields {
179 hint: Some(format!(
180 "cast it in SQL, e.g. ${index}::text::{}",
181 ty.name()
182 )),
183 ..DbErrorFields::new(
184 DbErrorKind::UnsupportedType,
185 format!("parameter ${index}: type {} is not supported", ty.name()),
186 )
187 }
188 .into(),
189 })
190}
191
192fn describe(param: &Param) -> String {
194 let text = match param {
195 Param::Text(s)
196 | Param::Uuid(s)
197 | Param::Timestamp(s)
198 | Param::NaiveTimestamp(s)
199 | Param::Date(s)
200 | Param::Time(s)
201 | Param::Numeric(s) => Some(s),
202 _ => None,
203 };
204 match text {
205 Some(s) if s.chars().count() > 40 => {
206 format!(
207 "{} '{}…'",
208 param.tag(),
209 s.chars().take(40).collect::<String>()
210 )
211 }
212 Some(s) => format!("{} '{s}'", param.tag()),
213 None => param.tag().to_string(),
214 }
215}
216
217fn coerce_to(param: &Param, ty: &Type) -> Coerced {
220 let ty = base_type(ty);
221 match ty.kind() {
222 Kind::Array(element) => return coerce_array(param, element),
223 Kind::Enum(_) => {
224 return match param {
225 Param::Null => Ok(Bound::Null),
226 Param::Text(label) => Ok(Bound::Text(label.clone())),
227 _ => Err(mismatch()),
228 }
229 }
230 _ => {}
231 }
232 if *param == Param::Null {
233 return Ok(Bound::Null);
234 }
235 match *ty {
236 Type::BOOL => match param {
237 Param::Bool(v) => Ok(Bound::Bool(*v)),
238 _ => Err(mismatch()),
239 },
240 Type::INT2 => integer(param)
241 .and_then(|v| i16::try_from(v).map_err(|_| invalid("out of range for int2")))
242 .map(Bound::I16),
243 Type::INT4 => integer(param)
244 .and_then(|v| i32::try_from(v).map_err(|_| invalid("out of range for int4")))
245 .map(Bound::I32),
246 Type::INT8 => integer(param).map(Bound::I64),
247 Type::FLOAT4 => float4(param).map(Bound::F32),
248 Type::FLOAT8 => float8(param).map(Bound::F64),
249 Type::NUMERIC => numeric(param).map(Bound::Numeric),
250 Type::TEXT | Type::VARCHAR | Type::BPCHAR | Type::NAME => text(param).map(Bound::Text),
251 Type::UUID => uuid(param).map(Bound::Uuid),
252 Type::TIMESTAMPTZ => match param {
253 Param::Timestamp(s) | Param::Text(s) => rfc3339(s).map(Bound::Timestamptz),
254 _ => Err(mismatch()),
255 },
256 Type::TIMESTAMP => match param {
257 Param::NaiveTimestamp(s) | Param::Text(s) => naive_timestamp(s).map(Bound::Timestamp),
258 Param::Timestamp(s) => rfc3339(s).map(|t| Bound::Timestamp(t.naive_utc())),
259 _ => Err(mismatch()),
260 },
261 Type::DATE => match param {
262 Param::Date(s) | Param::Text(s) => NaiveDate::parse_from_str(s, "%Y-%m-%d")
263 .map(Bound::Date)
264 .map_err(invalid),
265 _ => Err(mismatch()),
266 },
267 Type::TIME => match param {
268 Param::Time(s) | Param::Text(s) => NaiveTime::parse_from_str(s, "%H:%M:%S%.f")
269 .map(Bound::Time)
270 .map_err(invalid),
271 _ => Err(mismatch()),
272 },
273 Type::JSON | Type::JSONB => json(param).map(Bound::Json),
274 Type::BYTEA => match param {
275 Param::Bytes(v) => Ok(Bound::Bytes(v.clone())),
276 _ => Err(mismatch()),
277 },
278 Type::UNKNOWN => match param {
279 Param::Text(s) => Ok(Bound::Text(s.clone())),
280 _ => Err(mismatch()),
281 },
282 _ if ty.name() == "citext" => text(param).map(Bound::Text),
283 _ => Err(Refusal::Unsupported),
284 }
285}
286
287fn coerce_array(param: &Param, element: &Type) -> Coerced {
288 if *param == Param::Null {
289 return Ok(Bound::Null);
290 }
291 let element = base_type(element);
292 if let Kind::Enum(_) = element.kind() {
293 return match param {
294 Param::TextArray(values) => Ok(Bound::TextArray(values.clone())),
295 _ => Err(mismatch()),
296 };
297 }
298 match (element, param) {
299 (&Type::TEXT | &Type::VARCHAR | &Type::BPCHAR | &Type::NAME, Param::TextArray(v))
300 | (&Type::TEXT | &Type::VARCHAR | &Type::BPCHAR | &Type::NAME, Param::UuidArray(v)) => {
301 Ok(Bound::TextArray(v.clone()))
302 }
303 (&Type::UUID, Param::UuidArray(v) | Param::TextArray(v)) => {
304 each(v, |s| Uuid::parse_str(s).map_err(invalid)).map(Bound::UuidArray)
305 }
306 (&Type::INT2 | &Type::INT4 | &Type::INT8, Param::TextArray(v)) => {
307 each(v, |s| integer(&Param::Text(s.clone())))
308 .and_then(|integers| coerce_array(&Param::I64Array(integers), element))
309 }
310 (&Type::INT2, Param::I32Array(v)) => each(v, narrow::<i32, i16>).map(Bound::I16Array),
311 (&Type::INT2, Param::I64Array(v)) => each(v, narrow::<i64, i16>).map(Bound::I16Array),
312 (&Type::INT4, Param::I32Array(v)) => Ok(Bound::I32Array(v.clone())),
313 (&Type::INT4, Param::I64Array(v)) => each(v, narrow::<i64, i32>).map(Bound::I32Array),
314 (&Type::INT8, Param::I32Array(v)) => Ok(Bound::I64Array(
315 v.iter().map(|x| x.map(i64::from)).collect(),
316 )),
317 (&Type::INT8, Param::I64Array(v)) => Ok(Bound::I64Array(v.clone())),
318 (&Type::BOOL, Param::BoolArray(v)) => Ok(Bound::BoolArray(v.clone())),
319 (&Type::FLOAT8, Param::F64Array(v)) => Ok(Bound::F64Array(v.clone())),
320 (&Type::FLOAT8, Param::TextArray(v)) => {
321 each(v, |s| float8(&Param::Text(s.clone()))).map(Bound::F64Array)
322 }
323 (&Type::NUMERIC, Param::NumericArray(v) | Param::TextArray(v)) => {
324 each(v, |s| s.parse::<PgNumeric>().map_err(invalid)).map(Bound::NumericArray)
325 }
326 (&Type::JSON | &Type::JSONB, Param::Json(Value::Array(v))) => {
327 Ok(Bound::JsonArray(v.iter().cloned().map(Some).collect()))
328 }
329 (
330 &Type::TEXT
331 | &Type::VARCHAR
332 | &Type::BPCHAR
333 | &Type::NAME
334 | &Type::UUID
335 | &Type::INT2
336 | &Type::INT4
337 | &Type::INT8
338 | &Type::BOOL
339 | &Type::FLOAT8
340 | &Type::NUMERIC
341 | &Type::JSON
342 | &Type::JSONB,
343 _,
344 ) => Err(mismatch()),
345 _ => Err(Refusal::Unsupported),
346 }
347}
348
349fn each<T, U>(
350 values: &[Option<T>],
351 convert: impl Fn(&T) -> Result<U, Refusal>,
352) -> Result<Vec<Option<U>>, Refusal> {
353 values
354 .iter()
355 .map(|v| v.as_ref().map(&convert).transpose())
356 .collect()
357}
358
359fn narrow<T: Copy, U: TryFrom<T>>(value: &T) -> Result<U, Refusal> {
360 U::try_from(*value).map_err(|_| invalid("element out of range"))
361}
362
363fn integer(param: &Param) -> Result<i64, Refusal> {
364 match param {
365 Param::I16(v) => Ok(i64::from(*v)),
366 Param::I32(v) => Ok(i64::from(*v)),
367 Param::I64(v) => Ok(*v),
368 Param::Text(s) => s.trim().parse().map_err(|_| invalid("not an integer")),
369 Param::Numeric(s) => s
370 .parse::<PgNumeric>()
371 .ok()
372 .and_then(|n| n.to_i64())
373 .ok_or_else(|| invalid("not an integer in range")),
374 _ => Err(mismatch()),
375 }
376}
377
378trait Float: FromStr + Copy + PartialEq {
380 const ZERO: Self;
381 fn is_finite(self) -> bool;
382}
383
384impl Float for f32 {
385 const ZERO: Self = 0.0;
386 fn is_finite(self) -> bool {
387 f32::is_finite(self)
388 }
389}
390
391impl Float for f64 {
392 const ZERO: Self = 0.0;
393 fn is_finite(self) -> bool {
394 f64::is_finite(self)
395 }
396}
397
398fn parse_float<F: Float>(text: &str) -> Result<F, Refusal> {
402 let text = text.trim();
403 let value = text.parse::<F>().map_err(|_| invalid("not a number"))?;
404 let spelled_out = matches!(
405 text.trim_start_matches(['+', '-'])
406 .to_ascii_lowercase()
407 .as_str(),
408 "nan" | "inf" | "infinity"
409 );
410 let mantissa_is_zero = text
411 .split(['e', 'E'])
412 .next()
413 .is_none_or(|mantissa| !mantissa.bytes().any(|b| (b'1'..=b'9').contains(&b)));
414 match value {
415 v if !v.is_finite() && !spelled_out => Err(invalid("out of range")),
416 v if v == F::ZERO && !mantissa_is_zero => Err(invalid("out of range")),
417 v => Ok(v),
418 }
419}
420
421fn exact_integer<F: Float>(
423 value: i64,
424 to_float: fn(i64) -> F,
425 to_int: fn(F) -> i128,
426) -> Result<F, Refusal> {
427 let float = to_float(value);
428 (to_int(float) == i128::from(value))
429 .then_some(float)
430 .ok_or_else(|| invalid("not exactly representable as a float"))
431}
432
433fn float8(param: &Param) -> Result<f64, Refusal> {
434 let exact = |v: i64| exact_integer(v, |v| v as f64, |f| f as i128);
435 match param {
436 Param::F32(v) => Ok(f64::from(*v)),
437 Param::F64(v) => Ok(*v),
438 Param::I16(v) => Ok(f64::from(*v)),
439 Param::I32(v) => Ok(f64::from(*v)),
440 Param::I64(v) => exact(*v),
441 Param::Numeric(s) | Param::Text(s) => parse_float(s),
442 _ => Err(mismatch()),
443 }
444}
445
446fn float4(param: &Param) -> Result<f32, Refusal> {
449 let exact = |v: i64| exact_integer(v, |v| v as f32, |f| f as i128);
450 match param {
451 Param::F32(v) => Ok(*v),
452 Param::F64(v) => {
453 let narrowed = *v as f32;
454 let overflow = v.is_finite() && narrowed.is_infinite();
455 let underflow = *v != 0.0 && narrowed == 0.0;
456 (!overflow && !underflow)
457 .then_some(narrowed)
458 .ok_or_else(|| invalid("out of range for float4"))
459 }
460 Param::I16(v) => Ok(f32::from(*v)),
461 Param::I32(v) => exact(i64::from(*v)),
462 Param::I64(v) => exact(*v),
463 Param::Numeric(s) | Param::Text(s) => parse_float(s),
464 _ => Err(mismatch()),
465 }
466}
467
468fn numeric(param: &Param) -> Result<PgNumeric, Refusal> {
469 let finite = |v: f64, shortest: String| {
470 v.is_finite()
471 .then_some(shortest)
472 .ok_or_else(|| invalid("not finite"))
473 };
474 let text = match param {
475 Param::Numeric(s) | Param::Text(s) => s.clone(),
476 Param::I16(v) => v.to_string(),
477 Param::I32(v) => v.to_string(),
478 Param::I64(v) => v.to_string(),
479 Param::F64(v) => finite(*v, v.to_string())?,
480 Param::F32(v) => finite(f64::from(*v), v.to_string())?,
481 _ => return Err(mismatch()),
482 };
483 text.parse().map_err(invalid)
484}
485
486fn text(param: &Param) -> Result<String, Refusal> {
487 match param {
488 Param::Text(s) | Param::Uuid(s) => Ok(s.clone()),
489 _ => Err(mismatch()),
490 }
491}
492
493fn uuid(param: &Param) -> Result<Uuid, Refusal> {
494 match param {
495 Param::Uuid(s) | Param::Text(s) => Uuid::parse_str(s).map_err(|_| invalid("invalid uuid")),
496 _ => Err(mismatch()),
497 }
498}
499
500fn rfc3339(s: &str) -> Result<DateTime<Utc>, Refusal> {
501 DateTime::parse_from_rfc3339(s)
502 .map(|t| t.with_timezone(&Utc))
503 .map_err(|_| invalid("not an RFC 3339 timestamp with an offset"))
504}
505
506fn naive_timestamp(s: &str) -> Result<NaiveDateTime, Refusal> {
507 ["%Y-%m-%dT%H:%M:%S%.f", "%Y-%m-%d %H:%M:%S%.f"]
508 .iter()
509 .find_map(|format| NaiveDateTime::parse_from_str(s, format).ok())
510 .ok_or_else(|| invalid("not a timestamp without an offset"))
511}
512
513fn json(param: &Param) -> Result<Value, Refusal> {
514 Ok(match param {
515 Param::Json(v) => v.clone(),
516 Param::Bool(v) => Value::Bool(*v),
517 Param::I16(v) => Value::from(*v),
518 Param::I32(v) => Value::from(*v),
519 Param::I64(v) => Value::from(*v),
520 Param::F32(v) => serde_json::Number::from_f64(f64::from(*v))
521 .map(Value::Number)
522 .ok_or_else(|| invalid("not finite"))?,
523 Param::F64(v) => serde_json::Number::from_f64(*v)
524 .map(Value::Number)
525 .ok_or_else(|| invalid("not finite"))?,
526 Param::Text(s)
527 | Param::Uuid(s)
528 | Param::Timestamp(s)
529 | Param::NaiveTimestamp(s)
530 | Param::Date(s)
531 | Param::Time(s)
532 | Param::Numeric(s) => Value::String(s.clone()),
533 _ => return Err(mismatch()),
534 })
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540 use serde_json::json;
541
542 fn ok(param: Param, ty: &Type) -> Bound {
543 coerce(1, ¶m, ty).unwrap_or_else(|e| panic!("{ty} from {param:?}: {e}"))
544 }
545
546 fn err(param: Param, ty: &Type) -> DbError {
547 match coerce(3, ¶m, ty) {
548 Ok(bound) => panic!("{ty} accepted {param:?} as {bound:?}"),
549 Err(e) => e,
550 }
551 }
552
553 fn enum_type() -> Type {
554 Type::new(
555 "mood".into(),
556 90_001,
557 Kind::Enum(vec!["sad".into(), "happy".into()]),
558 "public".into(),
559 )
560 }
561
562 fn domain_over(base: Type) -> Type {
563 Type::new(
564 format!("{}_domain", base.name()),
565 90_002,
566 Kind::Domain(base),
567 "public".into(),
568 )
569 }
570
571 #[test]
572 fn integers_are_range_checked_and_parsed_exactly() {
573 assert_eq!(ok(Param::I64(7), &Type::INT2), Bound::I16(7));
574 assert_eq!(ok(Param::I16(7), &Type::INT8), Bound::I64(7));
575 assert_eq!(ok(Param::Text(" 42 ".into()), &Type::INT4), Bound::I32(42));
576 assert_eq!(
577 ok(Param::Numeric("42.000".into()), &Type::INT8),
578 Bound::I64(42)
579 );
580 let range = err(Param::I64(70_000), &Type::INT2);
581 assert_eq!(range.kind, DbErrorKind::InvalidParameter);
582 assert_eq!(
583 range.message,
584 "parameter $3: expected int2, got i64 (out of range for int2)"
585 );
586 err(Param::I64(i64::from(i32::MAX) + 1), &Type::INT4);
587 err(Param::Numeric("1.5".into()), &Type::INT4);
588 err(Param::Text("1.5".into()), &Type::INT4);
589 err(Param::F64(1.0), &Type::INT4);
590 err(Param::Bool(true), &Type::INT4);
591 }
592
593 #[test]
594 fn floats_take_numbers_that_fit_exactly() {
595 assert_eq!(ok(Param::F64(1.5), &Type::FLOAT8), Bound::F64(1.5));
596 assert_eq!(ok(Param::F64(0.5), &Type::FLOAT4), Bound::F32(0.5));
597 assert_eq!(
598 ok(Param::I64(1 << 53), &Type::FLOAT8),
599 Bound::F64(9_007_199_254_740_992.0)
600 );
601 assert_eq!(
602 ok(Param::I64(1 << 60), &Type::FLOAT8),
603 Bound::F64(1_152_921_504_606_846_976.0),
604 "a power of two is exact"
605 );
606 err(Param::I64((1 << 53) + 1), &Type::FLOAT8);
607 err(Param::I64(i64::MAX), &Type::FLOAT8);
608 assert_eq!(
609 ok(Param::Text("2.25".into()), &Type::FLOAT8),
610 Bound::F64(2.25)
611 );
612 assert!(
613 matches!(ok(Param::Numeric("NaN".into()), &Type::FLOAT8), Bound::F64(v) if v.is_nan())
614 );
615 err(Param::Text("two".into()), &Type::FLOAT8);
616 }
617
618 #[test]
619 fn float4_refuses_overflow_underflow_and_inexact_integers() {
620 assert_eq!(ok(Param::F64(0.1), &Type::FLOAT4), Bound::F32(0.1));
621 assert_eq!(ok(Param::F64(-0.0), &Type::FLOAT4), Bound::F32(-0.0));
622 assert_eq!(
623 ok(Param::Text("0.1".into()), &Type::FLOAT4),
624 ok(Param::F64(0.1), &Type::FLOAT4),
625 "text and float8 round alike"
626 );
627 for too_far in [1e300, -1e300, 1e-300, f64::from(f32::MAX) * 2.0] {
628 let error = err(Param::F64(too_far), &Type::FLOAT4);
629 assert!(error.message.contains("out of range"), "{too_far}: {error}");
630 }
631 for explicit in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
632 assert!(
633 matches!(ok(Param::F64(explicit), &Type::FLOAT4), Bound::F32(v) if v.is_nan() || v.is_infinite())
634 );
635 }
636 assert_eq!(
637 ok(Param::I32(1 << 24), &Type::FLOAT4),
638 Bound::F32(16_777_216.0)
639 );
640 err(Param::I32((1 << 24) + 1), &Type::FLOAT4);
641 assert!(err(Param::I64((1 << 40) + 1), &Type::FLOAT4)
642 .message
643 .contains("not exactly representable"));
644 assert_eq!(
645 ok(Param::I64(1 << 40), &Type::FLOAT4),
646 Bound::F32(1_099_511_627_776.0)
647 );
648 assert_eq!(ok(Param::I16(-7), &Type::FLOAT4), Bound::F32(-7.0));
649 }
650
651 #[test]
652 fn float_text_is_finite_unless_spelled_out() {
653 for (text, ty) in [
654 ("1e400", Type::FLOAT8),
655 ("-1e400", Type::FLOAT8),
656 ("1e-400", Type::FLOAT8),
657 ("1e40", Type::FLOAT4),
658 ("1e-50", Type::FLOAT4),
659 ] {
660 let error = err(Param::Text(text.into()), &ty);
661 assert!(error.message.contains("out of range"), "{text}: {error}");
662 }
663 for (text, ty) in [
664 ("0", Type::FLOAT4),
665 ("0.000e-999", Type::FLOAT8),
666 ("-0", Type::FLOAT8),
667 ] {
668 assert!(
669 matches!(ok(Param::Text(text.into()), &ty), Bound::F32(v) if v == 0.0)
670 || matches!(ok(Param::Text(text.into()), &ty), Bound::F64(v) if v == 0.0),
671 "{text}"
672 );
673 }
674 for spelled in [
675 "Infinity",
676 "-Infinity",
677 "inf",
678 "+INF",
679 "NaN",
680 "nan",
681 " infinity ",
682 ] {
683 assert!(
684 matches!(ok(Param::Text(spelled.into()), &Type::FLOAT8), Bound::F64(v) if !v.is_finite()),
685 "{spelled}"
686 );
687 assert!(
688 matches!(ok(Param::Numeric(spelled.into()), &Type::FLOAT4), Bound::F32(v) if !v.is_finite()),
689 "{spelled}"
690 );
691 }
692 }
693
694 #[test]
695 fn numerics_stay_exact() {
696 let n = |s: &str| Bound::Numeric(s.parse().expect("numeric"));
697 assert_eq!(ok(Param::Numeric("0.1".into()), &Type::NUMERIC), n("0.1"));
698 assert_eq!(ok(Param::Text("12.50".into()), &Type::NUMERIC), n("12.50"));
699 assert_eq!(ok(Param::I64(-3), &Type::NUMERIC), n("-3"));
700 assert_eq!(ok(Param::F64(0.1), &Type::NUMERIC), n("0.1"));
701 err(Param::F64(f64::NAN), &Type::NUMERIC);
702 err(Param::Text("1,5".into()), &Type::NUMERIC);
703 err(Param::Bool(true), &Type::NUMERIC);
704 }
705
706 #[test]
707 fn text_uuid_and_time_types_parse_their_grammar() {
708 let id = "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11";
709 assert_eq!(
710 ok(Param::Text("x".into()), &Type::VARCHAR),
711 Bound::Text("x".into())
712 );
713 assert_eq!(
714 ok(Param::Uuid(id.into()), &Type::TEXT),
715 Bound::Text(id.into())
716 );
717 err(Param::I32(1), &Type::TEXT);
718 let parsed = Uuid::parse_str(id).expect("uuid");
719 assert_eq!(ok(Param::Uuid(id.into()), &Type::UUID), Bound::Uuid(parsed));
720 assert_eq!(ok(Param::Text(id.into()), &Type::UUID), Bound::Uuid(parsed));
721 assert_eq!(
722 err(Param::Text("abc".into()), &Type::UUID).message,
723 "parameter $3: expected uuid, got text 'abc' (invalid uuid)"
724 );
725 let utc = DateTime::parse_from_rfc3339("2026-01-01T10:00:00Z")
726 .expect("ts")
727 .with_timezone(&Utc);
728 assert_eq!(
729 ok(
730 Param::Timestamp("2026-01-01T12:00:00+02:00".into()),
731 &Type::TIMESTAMPTZ
732 ),
733 Bound::Timestamptz(utc)
734 );
735 err(
736 Param::Timestamp("2026-01-01T12:00:00".into()),
737 &Type::TIMESTAMPTZ,
738 );
739 assert_eq!(
740 ok(
741 Param::Timestamp("2026-01-01T12:00:00+02:00".into()),
742 &Type::TIMESTAMP
743 ),
744 Bound::Timestamp(utc.naive_utc())
745 );
746 for naive in [
747 "2026-01-01T10:00:00",
748 "2026-01-01 10:00:00",
749 "2026-01-01 10:00:00.000",
750 ] {
751 assert_eq!(
752 ok(Param::NaiveTimestamp(naive.into()), &Type::TIMESTAMP),
753 Bound::Timestamp(utc.naive_utc()),
754 "{naive}"
755 );
756 }
757 assert_eq!(
758 ok(Param::Date("2026-02-03".into()), &Type::DATE),
759 Bound::Date(NaiveDate::from_ymd_opt(2026, 2, 3).expect("date"))
760 );
761 err(Param::Date("2026-02-30".into()), &Type::DATE);
762 assert_eq!(
763 ok(Param::Time("04:05:06.5".into()), &Type::TIME),
764 Bound::Time(NaiveTime::from_hms_milli_opt(4, 5, 6, 500).expect("time"))
765 );
766 }
767
768 #[test]
769 fn json_wraps_scalars_and_bytes_need_bytes() {
770 assert_eq!(
771 ok(Param::Json(json!({"a": 1})), &Type::JSONB),
772 Bound::Json(json!({"a": 1}))
773 );
774 assert_eq!(ok(Param::I32(1), &Type::JSON), Bound::Json(json!(1)));
775 assert_eq!(
776 ok(Param::Text("s".into()), &Type::JSONB),
777 Bound::Json(json!("s"))
778 );
779 err(Param::F64(f64::INFINITY), &Type::JSONB);
780 assert_eq!(
781 ok(Param::Bytes(vec![1]), &Type::BYTEA),
782 Bound::Bytes(vec![1])
783 );
784 err(Param::Text("AQ==".into()), &Type::BYTEA);
785 }
786
787 #[test]
788 fn nulls_bind_to_anything_supported() {
789 for ty in [
790 Type::INT4,
791 Type::TEXT,
792 Type::JSONB,
793 Type::UUID_ARRAY,
794 enum_type(),
795 ] {
796 assert_eq!(ok(Param::Null, &ty), Bound::Null, "{ty}");
797 }
798 }
799
800 #[test]
801 fn enums_domains_and_unknown_follow_their_rules() {
802 assert_eq!(
803 ok(Param::Text("happy".into()), &enum_type()),
804 Bound::Text("happy".into())
805 );
806 assert_eq!(
807 ok(Param::Text("added_later".into()), &enum_type()),
808 Bound::Text("added_later".into()),
809 "labels are the server's to check"
810 );
811 err(Param::I32(1), &enum_type());
812 let domain = domain_over(Type::INT4);
813 assert_eq!(ok(Param::I64(5), &domain), Bound::I32(5));
814 let nested = Type::new(
815 "small_positive".into(),
816 90_005,
817 Kind::Domain(domain),
818 "public".into(),
819 );
820 assert_eq!(ok(Param::I64(5), &nested), Bound::I32(5));
821 assert_eq!(
822 ok(Param::Text("sad".into()), &domain_over(enum_type())),
823 Bound::Text("sad".into())
824 );
825 assert_eq!(
826 ok(Param::Text("x".into()), &Type::UNKNOWN),
827 Bound::Text("x".into())
828 );
829 err(Param::I32(1), &Type::UNKNOWN);
830 }
831
832 #[test]
833 fn arrays_bind_element_by_element() {
834 let id = "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11";
835 assert_eq!(
836 ok(
837 Param::TextArray(vec![Some("a".into()), None]),
838 &Type::TEXT_ARRAY
839 ),
840 Bound::TextArray(vec![Some("a".into()), None])
841 );
842 assert_eq!(
843 ok(
844 Param::TextArray(vec![Some(id.into()), None]),
845 &Type::UUID_ARRAY
846 ),
847 Bound::UuidArray(vec![Some(Uuid::parse_str(id).expect("uuid")), None])
848 );
849 err(
850 Param::TextArray(vec![Some("nope".into())]),
851 &Type::UUID_ARRAY,
852 );
853 assert_eq!(
854 ok(Param::I32Array(vec![Some(1), None]), &Type::INT8_ARRAY),
855 Bound::I64Array(vec![Some(1), None])
856 );
857 assert_eq!(
858 ok(Param::I64Array(vec![Some(2)]), &Type::INT2_ARRAY),
859 Bound::I16Array(vec![Some(2)])
860 );
861 err(Param::I64Array(vec![Some(i64::MAX)]), &Type::INT4_ARRAY);
862 assert_eq!(
863 ok(
864 Param::TextArray(vec![Some(" 7".into()), None]),
865 &Type::INT4_ARRAY
866 ),
867 Bound::I32Array(vec![Some(7), None])
868 );
869 err(
870 Param::TextArray(vec![Some("seven".into())]),
871 &Type::INT8_ARRAY,
872 );
873 err(
874 Param::TextArray(vec![Some("70000".into())]),
875 &Type::INT2_ARRAY,
876 );
877 assert_eq!(
878 ok(
879 Param::TextArray(vec![Some("2.5".into())]),
880 &Type::FLOAT8_ARRAY
881 ),
882 Bound::F64Array(vec![Some(2.5)])
883 );
884 assert_eq!(
885 ok(
886 Param::TextArray(vec![Some("1.10".into())]),
887 &Type::NUMERIC_ARRAY
888 ),
889 Bound::NumericArray(vec![Some("1.10".parse().expect("numeric"))])
890 );
891 assert_eq!(
892 ok(Param::BoolArray(vec![None]), &Type::BOOL_ARRAY),
893 Bound::BoolArray(vec![None])
894 );
895 assert_eq!(
896 ok(Param::F64Array(vec![Some(1.5)]), &Type::FLOAT8_ARRAY),
897 Bound::F64Array(vec![Some(1.5)])
898 );
899 assert_eq!(
900 ok(
901 Param::NumericArray(vec![Some("1.10".into())]),
902 &Type::NUMERIC_ARRAY
903 ),
904 Bound::NumericArray(vec![Some("1.10".parse().expect("numeric"))])
905 );
906 assert_eq!(
907 ok(Param::Json(json!([1, "a"])), &Type::JSONB_ARRAY),
908 Bound::JsonArray(vec![Some(json!(1)), Some(json!("a"))])
909 );
910 let moods = Type::new(
911 "_mood".into(),
912 90_003,
913 Kind::Array(enum_type()),
914 "public".into(),
915 );
916 assert_eq!(
917 ok(Param::TextArray(vec![Some("sad".into())]), &moods),
918 Bound::TextArray(vec![Some("sad".into())])
919 );
920 assert_eq!(
921 ok(Param::TextArray(vec![Some("added".into())]), &moods),
922 Bound::TextArray(vec![Some("added".into())]),
923 "labels are the server's to check"
924 );
925 err(Param::I32Array(vec![Some(1)]), &Type::TEXT_ARRAY);
926 assert_eq!(
927 err(Param::TextArray(vec![]), &Type::POINT_ARRAY).kind,
928 DbErrorKind::UnsupportedType
929 );
930 }
931
932 #[test]
933 fn unsupported_types_suggest_a_cast() {
934 let error = err(Param::Text("(1,2)".into()), &Type::POINT);
935 assert_eq!(error.kind, DbErrorKind::UnsupportedType);
936 assert_eq!(error.message, "parameter $3: type point is not supported");
937 assert_eq!(
938 error.hint.as_deref(),
939 Some("cast it in SQL, e.g. $3::text::point")
940 );
941 }
942
943 #[test]
944 fn the_count_must_match_the_statement() {
945 let error = bind(&[Type::INT4], &[]).expect_err("count");
946 assert_eq!(error.kind, DbErrorKind::InvalidParameter);
947 assert_eq!(
948 error.message,
949 "the statement takes 1 parameter(s), 0 were given"
950 );
951 assert_eq!(
952 bind(
953 &[Type::INT4, Type::TEXT],
954 &[Param::I32(1), Param::Text("a".into())]
955 )
956 .expect("bound"),
957 vec![Bound::I32(1), Bound::Text("a".into())]
958 );
959 }
960
961 #[test]
962 fn long_values_are_previewed() {
963 let error = err(Param::Text("x".repeat(100)), &Type::UUID);
964 assert!(
965 error.message.contains(&format!("'{}…'", "x".repeat(40))),
966 "{}",
967 error.message
968 );
969 }
970
971 #[test]
972 fn bound_values_encode_as_their_type() {
973 let encode = |bound: Bound, ty: Type| {
974 let mut out = BytesMut::new();
975 let null = bound.to_sql_checked(&ty, &mut out).expect("encode");
976 (matches!(null, IsNull::Yes), out.to_vec())
977 };
978 assert_eq!(encode(Bound::Null, Type::INT4), (true, vec![]));
979 assert_eq!(encode(Bound::I32(1), Type::INT4), (false, vec![0, 0, 0, 1]));
980 assert_eq!(
981 encode(Bound::Text("happy".into()), enum_type()),
982 (false, b"happy".to_vec())
983 );
984 assert_eq!(
985 encode(Bound::Json(json!(1)), Type::JSONB),
986 (false, b"\x011".to_vec())
987 );
988 }
989
990 #[test]
991 fn domains_encode_as_their_base_type() {
992 let encode = |bound: Bound, ty: &Type| {
993 let mut out = BytesMut::new();
994 bound.to_sql_checked(ty, &mut out).expect("encode");
995 out.to_vec()
996 };
997 let jsonb = domain_over(Type::JSONB);
998 assert_eq!(
999 encode(Bound::Json(json!(1)), &jsonb),
1000 encode(Bound::Json(json!(1)), &Type::JSONB),
1001 "jsonb's version byte"
1002 );
1003 let tags = domain_over(Type::TEXT_ARRAY);
1004 assert_eq!(
1005 encode(Bound::TextArray(vec![Some("a".into())]), &tags),
1006 encode(Bound::TextArray(vec![Some("a".into())]), &Type::TEXT_ARRAY)
1007 );
1008 let int = domain_over(Type::INT4);
1009 assert_eq!(encode(Bound::I32(5), &int), vec![0, 0, 0, 5]);
1010
1011 let docs = Type::new(
1012 "_jsonb_domain".into(),
1013 90_004,
1014 Kind::Array(jsonb.clone()),
1015 "public".into(),
1016 );
1017 let encoded = encode(Bound::JsonArray(vec![Some(json!(1)), None]), &docs);
1018 let element_oid = u32::from_be_bytes(encoded[8..12].try_into().expect("oid"));
1019 assert_eq!(element_oid, jsonb.oid(), "the header names the domain");
1020 assert_eq!(&encoded[20..24], &2i32.to_be_bytes(), "element length");
1021 assert_eq!(&encoded[24..26], b"\x011", "a base-encoded jsonb element");
1022 assert_eq!(&encoded[26..30], &(-1i32).to_be_bytes(), "a null element");
1023
1024 let mut out = BytesMut::new();
1025 assert!(
1026 Bound::TextArray(vec![])
1027 .to_sql_checked(&Type::TEXT, &mut out)
1028 .is_err(),
1029 "an array sent as a scalar is an error, not a panic"
1030 );
1031 }
1032}