1use crate::numeric::PgNumeric;
2use base64::Engine;
3use cufflink_types::db::{DbError, DbErrorFields, DbErrorKind};
4use serde_json::{json, Value};
5use std::collections::HashSet;
6use std::error::Error;
7use tokio_postgres::types::{FromSql, Kind, Type};
8use tokio_postgres::{Column, Row};
9
10pub fn column_to_json(row: &Row, idx: usize, col_type: &Type) -> Value {
15 match *col_type {
16 Type::UUID => match row.try_get::<_, Option<uuid::Uuid>>(idx) {
17 Ok(Some(v)) => Value::String(v.to_string()),
18 _ => Value::Null,
19 },
20 Type::TEXT | Type::VARCHAR | Type::NAME | Type::BPCHAR | Type::UNKNOWN => {
21 match row.try_get::<_, Option<String>>(idx) {
22 Ok(Some(v)) => Value::String(v),
23 _ => Value::Null,
24 }
25 }
26 Type::INT2 => match row.try_get::<_, Option<i16>>(idx) {
27 Ok(Some(v)) => json!(v),
28 _ => Value::Null,
29 },
30 Type::INT4 => match row.try_get::<_, Option<i32>>(idx) {
31 Ok(Some(v)) => json!(v),
32 _ => Value::Null,
33 },
34 Type::INT8 => match row.try_get::<_, Option<i64>>(idx) {
35 Ok(Some(v)) => json!(v),
36 _ => Value::Null,
37 },
38 Type::FLOAT4 => match row.try_get::<_, Option<f32>>(idx) {
39 Ok(Some(v)) => json!(v),
40 _ => Value::Null,
41 },
42 Type::FLOAT8 => match row.try_get::<_, Option<f64>>(idx) {
43 Ok(Some(v)) => json!(v),
44 _ => Value::Null,
45 },
46 Type::BOOL => match row.try_get::<_, Option<bool>>(idx) {
47 Ok(Some(v)) => json!(v),
48 _ => Value::Null,
49 },
50 Type::DATE => match row.try_get::<_, Option<chrono::NaiveDate>>(idx) {
51 Ok(Some(v)) => Value::String(v.to_string()),
52 _ => Value::Null,
53 },
54 Type::TIMESTAMP => match row.try_get::<_, Option<chrono::NaiveDateTime>>(idx) {
55 Ok(Some(v)) => Value::String(v.to_string()),
56 _ => Value::Null,
57 },
58 Type::TIMESTAMPTZ => match row.try_get::<_, Option<chrono::DateTime<chrono::Utc>>>(idx) {
59 Ok(Some(v)) => Value::String(v.to_rfc3339()),
60 _ => Value::Null,
61 },
62 Type::JSON | Type::JSONB => match row.try_get::<_, Option<Value>>(idx) {
63 Ok(Some(v)) => v,
64 _ => Value::Null,
65 },
66 _ => match row.try_get::<_, Option<String>>(idx) {
67 Ok(Some(v)) => Value::String(v),
68 _ => Value::Null,
69 },
70 }
71}
72
73pub fn column_to_json_v2(row: &Row, idx: usize, ty: &Type) -> Result<Value, DbError> {
77 let name = row.columns().get(idx).map_or("?", |c| c.name());
78 let raw = row
79 .try_get::<_, RawValue>(idx)
80 .map_err(|e| DbError::new(DbErrorKind::Decode, format!("column {name}: {e}")))?;
81 raw.0
82 .map_or(Ok(Value::Null), |bytes| decode_v2(ty, bytes))
83 .map_err(|failure| failure.for_column(name, ty))
84}
85
86pub fn check_columns_v2(columns: &[Column]) -> Result<(), DbError> {
90 let mut seen = HashSet::new();
91 columns.iter().try_for_each(|column| {
92 let (name, ty) = (column.name(), column.type_());
93 if !renders_v2(ty) {
94 return Err(
95 DecodeFailure::Unsupported(format!("type {}", ty.name())).for_column(name, ty)
96 );
97 }
98 if !seen.insert(name) {
99 return Err(DbError::new(
100 DbErrorKind::NotSupported,
101 format!(
102 "column {name} appears more than once; give each column its own name (AS …)"
103 ),
104 ));
105 }
106 Ok(())
107 })
108}
109
110pub fn base_type(ty: &Type) -> &Type {
114 match ty.kind() {
115 Kind::Domain(inner) => base_type(inner),
116 _ => ty,
117 }
118}
119
120fn renders_v2(ty: &Type) -> bool {
122 let ty = base_type(ty);
123 match ty.kind() {
124 Kind::Enum(_) => true,
125 Kind::Array(element) => renders_v2(element),
126 _ => scalar_decoder(ty).is_some(),
127 }
128}
129
130struct RawValue<'a>(Option<&'a [u8]>);
132
133impl<'a> FromSql<'a> for RawValue<'a> {
134 fn from_sql(_: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
135 Ok(RawValue(Some(raw)))
136 }
137
138 fn from_sql_null(_: &Type) -> Result<Self, Box<dyn Error + Sync + Send>> {
139 Ok(RawValue(None))
140 }
141
142 fn accepts(_: &Type) -> bool {
143 true
144 }
145}
146
147enum DecodeFailure {
148 Unsupported(String),
149 Invalid(String),
150}
151
152impl DecodeFailure {
153 fn for_column(self, name: &str, ty: &Type) -> DbError {
154 match self {
155 DecodeFailure::Unsupported(what) => DbErrorFields {
156 hint: Some(format!("cast the column in SQL, e.g. {name}::text")),
157 ..DbErrorFields::new(
158 DbErrorKind::UnsupportedType,
159 format!("column {name}: {what} is not supported"),
160 )
161 }
162 .into(),
163 DecodeFailure::Invalid(e) => DbError::new(
164 DbErrorKind::Decode,
165 format!("column {name} ({}): {e}", ty.name()),
166 ),
167 }
168 }
169}
170
171fn decoded<'a, T: FromSql<'a>>(ty: &Type, raw: &'a [u8]) -> Result<T, DecodeFailure> {
172 T::from_sql(ty, raw).map_err(|e| DecodeFailure::Invalid(e.to_string()))
173}
174
175fn float_json(value: f64, shortest: String) -> Value {
176 match value {
177 v if v.is_nan() => Value::String("NaN".into()),
178 v if v.is_infinite() && v > 0.0 => Value::String("Infinity".into()),
179 v if v.is_infinite() => Value::String("-Infinity".into()),
180 _ => shortest
181 .parse::<f64>()
182 .ok()
183 .and_then(serde_json::Number::from_f64)
184 .map_or(Value::Null, Value::Number),
185 }
186}
187
188fn infinite<T: Copy + PartialEq + Bounded>(value: T) -> Option<Value> {
190 match value {
191 v if v == T::MAX => Some(Value::String("infinity".into())),
192 v if v == T::MIN => Some(Value::String("-infinity".into())),
193 _ => None,
194 }
195}
196
197trait Bounded {
198 const MAX: Self;
199 const MIN: Self;
200}
201
202impl Bounded for i64 {
203 const MAX: Self = i64::MAX;
204 const MIN: Self = i64::MIN;
205}
206
207impl Bounded for i32 {
208 const MAX: Self = i32::MAX;
209 const MIN: Self = i32::MIN;
210}
211
212const END_OF_DAY_MICROS: i64 = 86_400_000_000;
214
215fn decode_v2(ty: &Type, raw: &[u8]) -> Result<Value, DecodeFailure> {
216 let ty = base_type(ty);
217 match ty.kind() {
218 Kind::Enum(_) => decoded::<&str>(&Type::TEXT, raw).map(Value::from),
219 Kind::Array(element) => decode_array(element, raw),
220 _ => scalar_decoder(ty)
221 .ok_or_else(|| DecodeFailure::Unsupported(format!("type {}", ty.name())))
222 .and_then(|decode| decode(ty, raw)),
223 }
224}
225
226type Decoder = fn(&Type, &[u8]) -> Result<Value, DecodeFailure>;
227
228fn scalar_decoder(ty: &Type) -> Option<Decoder> {
231 Some(match *ty {
232 Type::INT2 => |ty, raw| Ok(json!(decoded::<i16>(ty, raw)?)),
233 Type::INT4 => |ty, raw| Ok(json!(decoded::<i32>(ty, raw)?)),
234 Type::INT8 => |ty, raw| Ok(json!(decoded::<i64>(ty, raw)?)),
235 Type::OID => |ty, raw| Ok(json!(decoded::<u32>(ty, raw)?)),
236 Type::FLOAT4 => |ty, raw| {
237 let v = decoded::<f32>(ty, raw)?;
238 Ok(float_json(f64::from(v), v.to_string()))
239 },
240 Type::FLOAT8 => |ty, raw| {
241 let v = decoded::<f64>(ty, raw)?;
242 Ok(float_json(v, v.to_string()))
243 },
244 Type::NUMERIC => |ty, raw| Ok(Value::String(decoded::<PgNumeric>(ty, raw)?.to_string())),
245 Type::BOOL => |ty, raw| Ok(Value::Bool(decoded(ty, raw)?)),
246 Type::TEXT | Type::VARCHAR | Type::BPCHAR | Type::NAME | Type::UNKNOWN => {
247 |ty, raw| Ok(Value::String(decoded(ty, raw)?))
248 }
249 Type::UUID => |ty, raw| Ok(Value::String(decoded::<uuid::Uuid>(ty, raw)?.to_string())),
250 Type::TIMESTAMPTZ => |ty, raw| {
251 Ok(match infinite(decoded::<i64>(&Type::INT8, raw)?) {
252 Some(v) => v,
253 None => Value::String(
254 decoded::<chrono::DateTime<chrono::Utc>>(ty, raw)?
255 .to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true),
256 ),
257 })
258 },
259 Type::TIMESTAMP => |ty, raw| {
260 Ok(match infinite(decoded::<i64>(&Type::INT8, raw)?) {
261 Some(v) => v,
262 None => Value::String(
263 decoded::<chrono::NaiveDateTime>(ty, raw)?
264 .format("%Y-%m-%dT%H:%M:%S%.f")
265 .to_string(),
266 ),
267 })
268 },
269 Type::DATE => |ty, raw| {
270 Ok(match infinite(decoded::<i32>(&Type::INT4, raw)?) {
271 Some(v) => v,
272 None => Value::String(
273 decoded::<chrono::NaiveDate>(ty, raw)?
274 .format("%Y-%m-%d")
275 .to_string(),
276 ),
277 })
278 },
279 Type::TIME => |ty, raw| {
280 Ok(match decoded::<i64>(&Type::INT8, raw)? {
281 END_OF_DAY_MICROS => Value::String("24:00:00".into()),
282 _ => Value::String(
283 decoded::<chrono::NaiveTime>(ty, raw)?
284 .format("%H:%M:%S%.f")
285 .to_string(),
286 ),
287 })
288 },
289 Type::INTERVAL => |ty, raw| Ok(Value::String(decoded::<PgInterval>(ty, raw)?.to_iso8601())),
290 Type::JSON | Type::JSONB => |ty, raw| decoded::<Value>(ty, raw),
291 Type::VOID => |_, _| Ok(Value::Null),
292 Type::BYTEA => |ty, raw| {
293 Ok(Value::String(
294 base64::engine::general_purpose::STANDARD.encode(decoded::<&[u8]>(ty, raw)?),
295 ))
296 },
297 _ if ty.name() == "citext" => |_, raw| {
298 Ok(Value::String(
299 decoded::<&str>(&Type::TEXT, raw)?.to_string(),
300 ))
301 },
302 _ => return None,
303 })
304}
305
306fn decode_array(element: &Type, raw: &[u8]) -> Result<Value, DecodeFailure> {
309 let truncated = || DecodeFailure::Invalid("truncated array value".into());
310 let int = |at: usize| -> Result<i32, DecodeFailure> {
311 raw.get(at..at + 4)
312 .and_then(|b| b.try_into().ok())
313 .map(i32::from_be_bytes)
314 .ok_or_else(truncated)
315 };
316 let (count, mut at) = match int(0)? {
317 0 => return Ok(Value::Array(Vec::new())),
318 1 => (int(12)?, 20usize),
319 dims => {
320 return Err(DecodeFailure::Unsupported(format!(
321 "a {dims}-dimensional array"
322 )))
323 }
324 };
325 (0..count)
326 .map(|_| {
327 let len = int(at)?;
328 at += 4;
329 match usize::try_from(len) {
330 Err(_) => Ok(Value::Null),
331 Ok(len) => {
332 let bytes = raw.get(at..at + len).ok_or_else(truncated)?;
333 at += len;
334 decode_v2(element, bytes)
335 }
336 }
337 })
338 .collect::<Result<Vec<_>, _>>()
339 .map(Value::Array)
340}
341
342#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub struct PgInterval {
346 pub months: i32,
347 pub days: i32,
348 pub micros: i64,
349}
350
351impl PgInterval {
352 pub fn to_iso8601(&self) -> String {
355 const MICROS_PER_SECOND: i64 = 1_000_000;
356 let date: String = [
357 (i64::from(self.months / 12), 'Y'),
358 (i64::from(self.months % 12), 'M'),
359 (i64::from(self.days), 'D'),
360 ]
361 .iter()
362 .filter(|(n, _)| *n != 0)
363 .map(|(n, unit)| format!("{n}{unit}"))
364 .collect();
365 let hours = self.micros / (3600 * MICROS_PER_SECOND);
366 let minutes = self.micros / (60 * MICROS_PER_SECOND) % 60;
367 let sub_minute = self.micros % (60 * MICROS_PER_SECOND);
368 let seconds = match sub_minute {
369 0 => String::new(),
370 micros => {
371 let sign = if micros < 0 { "-" } else { "" };
372 let (whole, frac) = (
373 micros.unsigned_abs() / MICROS_PER_SECOND as u64,
374 micros.unsigned_abs() % MICROS_PER_SECOND as u64,
375 );
376 let frac = format!("{frac:06}");
377 match frac.trim_end_matches('0') {
378 "" => format!("{sign}{whole}S"),
379 digits => format!("{sign}{whole}.{digits}S"),
380 }
381 }
382 };
383 let time: String = [(hours, 'H'), (minutes, 'M')]
384 .iter()
385 .filter(|(n, _)| *n != 0)
386 .map(|(n, unit)| format!("{n}{unit}"))
387 .chain(std::iter::once(seconds))
388 .collect();
389 match (date.is_empty(), time.is_empty()) {
390 (true, true) => "PT0S".into(),
391 (_, true) => format!("P{date}"),
392 _ => format!("P{date}T{time}"),
393 }
394 }
395}
396
397impl<'a> FromSql<'a> for PgInterval {
398 fn from_sql(_: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
399 if raw.len() != 16 {
400 return Err(format!("an interval is 16 bytes, got {}", raw.len()).into());
401 }
402 let (micros, rest) = raw.split_at(8);
403 let (days, months) = rest.split_at(4);
404 Ok(PgInterval {
405 micros: i64::from_be_bytes(micros.try_into()?),
406 days: i32::from_be_bytes(days.try_into()?),
407 months: i32::from_be_bytes(months.try_into()?),
408 })
409 }
410
411 fn accepts(ty: &Type) -> bool {
412 *ty == Type::INTERVAL
413 }
414}
415
416#[cfg(test)]
417mod v2_tests {
418 use super::*;
419 use crate::numeric::PgNumeric;
420 use bytes::BytesMut;
421 use tokio_postgres::types::ToSql;
422
423 fn encode<T: ToSql>(ty: &Type, value: T) -> Vec<u8> {
424 let mut out = BytesMut::new();
425 value.to_sql(ty, &mut out).expect("encode");
426 out.to_vec()
427 }
428
429 fn render<T: ToSql>(ty: &Type, value: T) -> Value {
430 decode_v2(ty, &encode(ty, value)).unwrap_or_else(|_| panic!("decode {ty}"))
431 }
432
433 fn failure(ty: &Type, raw: &[u8]) -> DbError {
434 match decode_v2(ty, raw) {
435 Ok(v) => panic!("{ty} decoded to {v}"),
436 Err(failure) => failure.for_column("c", ty),
437 }
438 }
439
440 #[test]
441 fn scalars_render_exactly() {
442 let uuid = uuid::Uuid::parse_str("A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11").expect("uuid");
443 let tz = chrono::DateTime::parse_from_rfc3339("2026-03-04T05:06:07.120+02:00")
444 .expect("timestamptz")
445 .with_timezone(&chrono::Utc);
446 let naive = chrono::NaiveDate::from_ymd_opt(2026, 3, 4)
447 .and_then(|d| d.and_hms_micro_opt(5, 6, 7, 890))
448 .expect("timestamp");
449 for (value, expected) in [
450 (render(&Type::INT2, 7i16), json!(7)),
451 (render(&Type::INT4, -7i32), json!(-7)),
452 (render(&Type::INT8, i64::MAX), json!(i64::MAX)),
453 (render(&Type::OID, 42u32), json!(42)),
454 (render(&Type::FLOAT4, 0.1f32), json!(0.1)),
455 (render(&Type::FLOAT8, 2.5f64), json!(2.5)),
456 (render(&Type::FLOAT8, f64::NAN), json!("NaN")),
457 (render(&Type::FLOAT8, f64::INFINITY), json!("Infinity")),
458 (render(&Type::FLOAT4, f32::NEG_INFINITY), json!("-Infinity")),
459 (
460 render(
461 &Type::NUMERIC,
462 "12345678901234567890.000100"
463 .parse::<PgNumeric>()
464 .expect("n"),
465 ),
466 json!("12345678901234567890.000100"),
467 ),
468 (render(&Type::BOOL, true), json!(true)),
469 (render(&Type::TEXT, "it's"), json!("it's")),
470 (render(&Type::BPCHAR, "ab"), json!("ab")),
471 (
472 render(&Type::UUID, uuid),
473 json!("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"),
474 ),
475 (
476 render(&Type::TIMESTAMPTZ, tz),
477 json!("2026-03-04T03:06:07.120Z"),
478 ),
479 (
480 render(&Type::TIMESTAMP, naive),
481 json!("2026-03-04T05:06:07.000890"),
482 ),
483 (
484 render(
485 &Type::TIMESTAMP,
486 naive.date().and_hms_opt(1, 2, 3).expect("t"),
487 ),
488 json!("2026-03-04T01:02:03"),
489 ),
490 (render(&Type::DATE, naive.date()), json!("2026-03-04")),
491 (render(&Type::TIME, naive.time()), json!("05:06:07.000890")),
492 (
493 render(&Type::JSONB, json!({"a": [1, null]})),
494 json!({"a": [1, null]}),
495 ),
496 (render(&Type::JSON, json!("s")), json!("s")),
497 (render(&Type::BYTEA, &b"\x00\xff"[..]), json!("AP8=")),
498 ] {
499 assert_eq!(value, expected);
500 }
501 }
502
503 #[test]
504 fn void_results_render_as_null() {
505 assert_eq!(decode_v2(&Type::VOID, &[]).ok(), Some(Value::Null));
506 }
507
508 #[test]
509 fn infinite_dates_and_end_of_day_render_as_postgres_prints_them() {
510 assert_eq!(
511 decode_v2(&Type::TIMESTAMPTZ, &encode(&Type::INT8, i64::MAX)).ok(),
512 Some(json!("infinity"))
513 );
514 assert_eq!(
515 decode_v2(&Type::TIMESTAMP, &encode(&Type::INT8, i64::MIN)).ok(),
516 Some(json!("-infinity"))
517 );
518 assert_eq!(
519 decode_v2(&Type::DATE, &encode(&Type::INT4, i32::MAX)).ok(),
520 Some(json!("infinity"))
521 );
522 assert_eq!(
523 decode_v2(&Type::TIME, &encode(&Type::INT8, END_OF_DAY_MICROS)).ok(),
524 Some(json!("24:00:00"))
525 );
526 }
527
528 #[test]
529 fn intervals_render_as_iso_8601() {
530 let interval = |months, days, micros| {
531 PgInterval {
532 months,
533 days,
534 micros,
535 }
536 .to_iso8601()
537 };
538 assert_eq!(interval(14, 3, 14_706_500_000), "P1Y2M3DT4H5M6.5S");
539 assert_eq!(interval(0, 0, 0), "PT0S");
540 assert_eq!(interval(0, 1, 0), "P1D");
541 assert_eq!(interval(-14, 0, 0), "P-1Y-2M");
542 assert_eq!(interval(0, 0, -1_500_000), "PT-1.5S");
543 assert_eq!(interval(0, 0, 3_600_000_000), "PT1H");
544 assert_eq!(interval(0, 0, 60_000_001), "PT1M0.000001S");
545 let mut raw = Vec::new();
546 raw.extend_from_slice(&14_706_500_000i64.to_be_bytes());
547 raw.extend_from_slice(&3i32.to_be_bytes());
548 raw.extend_from_slice(&14i32.to_be_bytes());
549 assert_eq!(
550 decode_v2(&Type::INTERVAL, &raw).ok(),
551 Some(json!("P1Y2M3DT4H5M6.5S"))
552 );
553 assert_eq!(
554 failure(&Type::INTERVAL, &raw[..15]).kind,
555 DbErrorKind::Decode
556 );
557 }
558
559 #[test]
560 fn arrays_enums_and_domains_render_through_their_element_type() {
561 assert_eq!(
562 render(&Type::INT4_ARRAY, vec![Some(1i32), None, Some(3)]),
563 json!([1, null, 3])
564 );
565 assert_eq!(
566 render(&Type::TEXT_ARRAY, Vec::<Option<String>>::new()),
567 json!([])
568 );
569 assert_eq!(
570 render(
571 &Type::NUMERIC_ARRAY,
572 vec![Some("0.10".parse::<PgNumeric>().expect("n")), None]
573 ),
574 json!(["0.10", null])
575 );
576 let mood = Type::new(
577 "mood".into(),
578 90_001,
579 Kind::Enum(vec!["sad".into(), "happy".into()]),
580 "public".into(),
581 );
582 assert_eq!(decode_v2(&mood, b"happy").ok(), Some(json!("happy")));
583 let moods = Type::new("_mood".into(), 90_002, Kind::Array(mood), "public".into());
584 let mut raw = Vec::new();
585 for word in [1i32, 1, 90_001, 2, 1] {
586 raw.extend_from_slice(&word.to_be_bytes());
587 }
588 raw.extend_from_slice(&3i32.to_be_bytes());
589 raw.extend_from_slice(b"sad");
590 raw.extend_from_slice(&(-1i32).to_be_bytes());
591 assert_eq!(decode_v2(&moods, &raw).ok(), Some(json!(["sad", null])));
592 let positive = Type::new(
593 "positive".into(),
594 90_003,
595 Kind::Domain(Type::INT4),
596 "public".into(),
597 );
598 assert_eq!(
599 decode_v2(&positive, &encode(&Type::INT4, 5i32)).ok(),
600 Some(json!(5))
601 );
602 }
603
604 #[test]
605 fn unsupported_and_corrupt_values_are_errors_naming_the_column() {
606 let point = failure(&Type::POINT, &[0; 16]);
607 assert_eq!(point.kind, DbErrorKind::UnsupportedType);
608 assert!(point.message.contains("column c"), "{}", point.message);
609 assert!(point.message.contains("point"), "{}", point.message);
610 assert!(point.hint.as_deref().is_some_and(|h| h.contains("c::text")));
611 let mut matrix = Vec::new();
612 for word in [2i32, 0, 23, 1, 1, 1, 1] {
613 matrix.extend_from_slice(&word.to_be_bytes());
614 }
615 assert_eq!(
616 failure(&Type::INT4_ARRAY, &matrix).kind,
617 DbErrorKind::UnsupportedType
618 );
619 assert_eq!(failure(&Type::INT4, &[0, 1]).kind, DbErrorKind::Decode);
620 assert_eq!(
621 failure(&Type::INT4_ARRAY, &[0, 0, 0, 1]).kind,
622 DbErrorKind::Decode
623 );
624 }
625}