Skip to main content

cufflink_pgcodec/
row_json.rs

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
10/// Convert a single Postgres column into a `serde_json::Value`, mapping common
11/// scalar types faithfully. Unknown types fall back to the column's text
12/// representation. Used by both the SQL query endpoint (streaming rows to the
13/// CLI) and the WASM runtime's host DB bridge.
14pub 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
73/// A column's value in the `db_call` v2 row envelope: every supported type
74/// rendered exactly, and an error naming the column rather than a silent
75/// `null` for anything else.
76pub 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
86/// Refuse, before a statement runs, result columns the v2 envelope cannot
87/// carry: a type it does not render, or a name that appears twice (a row is
88/// keyed by column name).
89pub 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
110/// A type with its domains looked through. tokio-postgres resolves a
111/// domain's `typbasetype` once per connection, and a domain's base type
112/// never changes, so the cached `Kind::Domain` is always current.
113pub fn base_type(ty: &Type) -> &Type {
114    match ty.kind() {
115        Kind::Domain(inner) => base_type(inner),
116        _ => ty,
117    }
118}
119
120/// Whether the v2 envelope renders values of `ty`.
121fn 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
130/// A column's raw wire bytes, whatever its type; `None` for SQL `NULL`.
131struct 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
188/// `infinity` and `-infinity` timestamps and dates, which chrono cannot hold.
189fn 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
212/// `24:00:00`, which Postgres accepts as a `time` and chrono wraps to midnight.
213const 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
228/// How a scalar type's values render; `None` for a type the v2 envelope
229/// does not carry.
230fn 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
306/// A one-dimensional array: `[ndim][flags][element oid]([len][lower bound])*`
307/// then each element as `[len or -1][bytes]`.
308fn 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/// A Postgres `interval`: months, days and microseconds kept apart, as the
343/// server stores them.
344#[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    /// ISO 8601 as Postgres's `iso_8601` interval style prints it: each
353    /// component carries its own sign, and a zero interval is `PT0S`.
354    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}