Skip to main content

pylon_pgcon/
wire.rs

1//
2// This source file is part of the Pylon open source project.
3//
4// Copyright (c) 2026 Jaldis B.V.
5//
6// Licensed under the MIT OR Apache-2.0 license (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10//     https://opensource.org/licenses/MIT
11//     https://www.apache.org/licenses/LICENSE-2.0
12//
13// Unless required by applicable law or agreed to in writing, software
14// distributed under the License is distributed on an "AS IS" BASIS,
15// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16// See the License for the specific language governing permissions and
17// limitations under the License.
18//
19
20//! Recursive decoder from PostgreSQL's binary wire format into
21//! `pylon_value::DecodedValue` — the shared decode target `pylon-cache` also
22//! stores, so a cache hit and a fresh row decode into the exact same shape.
23//!
24//! Every PyQL query result is emitted by `pylon-core` as a single
25//! `SELECT (...) AS result` — an anonymous composite (`record`, OID 2249).
26//! `tokio-postgres` has no generic "decode any composite into a value tree"
27//! API (its `FromSql` machinery targets known Rust types), so this module
28//! walks PostgreSQL's own documented binary wire format directly, the same
29//! way the outgoing Python implementation's `_pg_decode_record`/
30//! `_pg_decode_value` (`pylon/client.py`) did — except comprehensive,
31//! comprehensively, including the cases a generic composite decoder plus
32//! hand-registered codec overrides tends to miss (jsonb, `record[]`,
33//! `vector`).
34//!
35//! Composite field layout (used recursively for `record`/`record[]`):
36//! `i32 nfields`, then per field: `u32 type_oid`, `i32 field_len`
37//! (`-1` = NULL), `field_len` bytes of that field's own wire encoding.
38//! Array layout (used for every `T[]` OID below): `i32 ndim`, `i32
39//! has_null_flag`, `u32 element_oid`, then *one* `(i32 dim_size, i32
40//! lower_bound)` pair — Pylon's `pylon.Array[T]` is always 1-dimensional,
41//! so multi-dimensional arrays are out of scope, same as the Python
42//! implementation this replaces — then per element: `i32 len` (`-1` =
43//! NULL) + `len` bytes.
44
45use pylon_value::DecodedValue;
46
47use crate::numeric;
48
49pub use crate::error::Error;
50pub type Result<T> = crate::Result<T>;
51
52// Fixed, well-known OIDs (see `pg_type.h` / `SELECT oid, typname FROM
53// pg_type`) — stable across every Postgres install, unlike extension types
54// (`vector`, PostGIS geometry/geography), enums, and domains, whose OIDs are
55// assigned by the local database and are discovered per-connection instead
56// (see `ExtensionOids` and `TYPE_DISCOVERY_SQL`).
57const OID_BOOL: u32 = 16;
58const OID_BYTEA: u32 = 17;
59const OID_INT8: u32 = 20;
60const OID_INT2: u32 = 21;
61const OID_INT4: u32 = 23;
62const OID_TEXT: u32 = 25;
63const OID_JSONB: u32 = 3802;
64const OID_FLOAT4: u32 = 700;
65const OID_FLOAT8: u32 = 701;
66const OID_BPCHAR: u32 = 1042;
67const OID_VARCHAR: u32 = 1043;
68const OID_NUMERIC: u32 = 1700;
69const OID_DATE: u32 = 1082;
70const OID_TIME: u32 = 1083;
71const OID_TIMESTAMP: u32 = 1114;
72const OID_TIMESTAMPTZ: u32 = 1184;
73const OID_INTERVAL: u32 = 1186;
74const OID_UUID: u32 = 2950;
75const OID_RECORD: u32 = 2249;
76const OID_RECORD_ARRAY: u32 = 2287;
77/// The pseudo-type Postgres reports for a literal it never had to resolve to
78/// a concrete type — `SELECT ('doc', ...)` inside a row constructor, for
79/// instance. Its wire format is the value's text representation, so it
80/// decodes exactly like `text`.
81const OID_UNKNOWN: u32 = 705;
82/// `name`, used by the catalogs (`pg_type.typname` and friends). Text-shaped
83/// on the wire, and reachable through the introspection queries.
84const OID_NAME: u32 = 19;
85
86const OID_BOOL_ARRAY: u32 = 1000;
87const OID_BYTEA_ARRAY: u32 = 1001;
88const OID_INT2_ARRAY: u32 = 1005;
89const OID_INT4_ARRAY: u32 = 1007;
90const OID_TEXT_ARRAY: u32 = 1009;
91const OID_BPCHAR_ARRAY: u32 = 1014;
92const OID_VARCHAR_ARRAY: u32 = 1015;
93const OID_INT8_ARRAY: u32 = 1016;
94const OID_FLOAT4_ARRAY: u32 = 1021;
95const OID_FLOAT8_ARRAY: u32 = 1022;
96const OID_NUMERIC_ARRAY: u32 = 1231;
97const OID_UUID_ARRAY: u32 = 2951;
98const OID_JSONB_ARRAY: u32 = 3807;
99
100// Native PostgreSQL range/multirange type OIDs, paired with the element
101// type OID their bound values decode with (int4range's bounds are int4,
102// etc.) — mirrors `range_ctor_for_pg_type`/`multirange_ctor_for_range_ctor`
103// in `pylon-core`'s `ir/compiler.rs`, the other side of this same "which 5
104// PG range families does Pylon support" decision.
105const OID_INT4RANGE: u32 = 3904;
106const OID_INT8RANGE: u32 = 3926;
107const OID_NUMRANGE: u32 = 3906;
108const OID_TSRANGE: u32 = 3908;
109const OID_TSTZRANGE: u32 = 3910;
110const OID_DATERANGE: u32 = 3912;
111const OID_INT4MULTIRANGE: u32 = 4451;
112const OID_INT8MULTIRANGE: u32 = 4536;
113const OID_NUMMULTIRANGE: u32 = 4532;
114const OID_TSMULTIRANGE: u32 = 4533;
115const OID_TSTZMULTIRANGE: u32 = 4534;
116const OID_DATEMULTIRANGE: u32 = 4535;
117
118/// The element OID a range/multirange type's bound values decode with —
119/// `None` for anything that isn't one of the 6 native range/multirange
120/// families this module knows about.
121fn range_element_oid(oid: u32) -> Option<u32> {
122    match oid {
123        OID_INT4RANGE | OID_INT4MULTIRANGE => Some(OID_INT4),
124        OID_INT8RANGE | OID_INT8MULTIRANGE => Some(OID_INT8),
125        OID_NUMRANGE | OID_NUMMULTIRANGE => Some(OID_NUMERIC),
126        OID_TSRANGE | OID_TSMULTIRANGE => Some(OID_TIMESTAMP),
127        OID_TSTZRANGE | OID_TSTZMULTIRANGE => Some(OID_TIMESTAMPTZ),
128        OID_DATERANGE | OID_DATEMULTIRANGE => Some(OID_DATE),
129        _ => None,
130    }
131}
132
133/// Per-database type OIDs, discovered once at connect time and threaded
134/// through decode calls.
135///
136/// Everything in here is assigned by the local database rather than fixed by
137/// `pg_type.h`: extension types get their OID at `CREATE EXTENSION` time,
138/// and enums and domains get theirs when the migration that declares them
139/// runs. The constants at the top of this module cover the built-in types
140/// whose OIDs are stable everywhere; this covers the rest.
141///
142/// `Default` means "nothing discovered yet", under which any OID not in the
143/// built-in set is an `Error::UnknownTypeOid` rather than a guess — see
144/// `decode_value`.
145/// `PartialEq` so `PgPool::heal_types` can tell whether anything moved.
146#[derive(Debug, Clone, Default, PartialEq, Eq)]
147pub struct ExtensionOids {
148    /// pgvector's `vector` type, if the extension is installed.
149    pub vector: Option<u32>,
150    /// Every enum type OID. Postgres sends an enum's binary value as its
151    /// label text, so these decode as `Str`.
152    pub enums: std::collections::HashSet<u32>,
153    /// Domain OID to the OID of the type it wraps. A domain's wire format is
154    /// its base type's, so these decode by recursing on the base.
155    pub domains: std::collections::HashMap<u32, u32>,
156    /// Array types whose element is one of the enums or domains above. The
157    /// element OID travels in the array's own binary header, so recognising
158    /// the array OID is all `decode_array` needs; without this an
159    /// `AuthenticationMethod[]` column is an `UnknownTypeOid` even though
160    /// every value in it would decode.
161    pub arrays: std::collections::HashSet<u32>,
162}
163
164/// One round trip that classifies every non-builtin type in the database:
165/// the `vector` extension type, all enums, all domains with the base type
166/// each resolves to, and the array types over any of those.
167///
168/// Arrays are reported with a `'A'` in the typtype column. Postgres never
169/// uses that letter itself (`b`, `c`, `d`, `e`, `m`, `p`, `r` are the real
170/// ones) -- it is `typcategory`'s letter for an array, borrowed here so the
171/// four-column shape holds for every row.
172pub(crate) const TYPE_DISCOVERY_SQL: &str = "\
173SELECT t.oid::int8, t.typtype::text, COALESCE(b.oid, 0)::int8, t.typname::text \
174FROM pg_type t \
175LEFT JOIN pg_type b ON b.oid = t.typbasetype \
176WHERE t.typtype IN ('e', 'd') OR t.typname = 'vector' \
177UNION ALL \
178SELECT a.oid::int8, 'A', e.oid::int8, a.typname::text \
179FROM pg_type a \
180JOIN pg_type e ON e.oid = a.typelem \
181WHERE a.typcategory = 'A' AND e.typtype IN ('e', 'd')";
182
183impl ExtensionOids {
184    /// Builds a registry from `TYPE_DISCOVERY_SQL`'s rows, given as
185    /// `(oid, typtype, base_oid, typname)`.
186    pub(crate) fn from_discovery_rows(rows: impl IntoIterator<Item = (u32, String, u32, String)>) -> Self {
187        let mut out = Self::default();
188        for (oid, typtype, base_oid, typname) in rows {
189            match typtype.as_str() {
190                "e" => {
191                    out.enums.insert(oid);
192                }
193                "d" if base_oid != 0 => {
194                    out.domains.insert(oid, base_oid);
195                }
196                "A" => {
197                    out.arrays.insert(oid);
198                }
199                _ => {}
200            }
201            // `vector` is a base type (typtype 'b'), so it only matches the
202            // name arm of the discovery query.
203            if typname == "vector" {
204                out.vector = Some(oid);
205            }
206        }
207        out
208    }
209}
210
211/// Decodes one field's raw buffer (already length-stripped, matching what
212/// `postgres_types::FromSql::from_sql` receives) into a `DecodedValue`,
213/// given its Postgres type OID. NULL is handled by the caller (a `-1`
214/// field length never reaches this function) — see `decode_record`/
215/// `decode_array` for where that's checked.
216pub fn decode_value(oid: u32, data: &[u8], ext: &ExtensionOids) -> Result<DecodedValue> {
217    if let Some(vector_oid) = ext.vector
218        && oid == vector_oid
219    {
220        return Ok(DecodedValue::Array(decode_vector(data)?));
221    }
222    match oid {
223        OID_BOOL => Ok(DecodedValue::Bool(data.first().copied().unwrap_or(0) != 0)),
224        OID_INT2 => Ok(DecodedValue::I64(i16::from_be_bytes(data.try_into()?) as i64)),
225        OID_INT4 => Ok(DecodedValue::I64(i32::from_be_bytes(data.try_into()?) as i64)),
226        OID_INT8 => Ok(DecodedValue::I64(i64::from_be_bytes(data.try_into()?))),
227        OID_FLOAT4 => Ok(DecodedValue::F64(f32::from_be_bytes(data.try_into()?) as f64)),
228        OID_FLOAT8 => Ok(DecodedValue::F64(f64::from_be_bytes(data.try_into()?))),
229        OID_TEXT | OID_VARCHAR | OID_BPCHAR | OID_UNKNOWN | OID_NAME => {
230            Ok(DecodedValue::Str(std::str::from_utf8(data)?.to_string()))
231        }
232        OID_UUID => {
233            let mut bytes = [0u8; 16];
234            bytes.copy_from_slice(data);
235            Ok(DecodedValue::Uuid(bytes))
236        }
237        OID_BYTEA => Ok(DecodedValue::Bytes(data.to_vec())),
238        OID_NUMERIC => decode_numeric(data),
239        OID_INTERVAL => decode_interval(data),
240        OID_DATE => Ok(DecodedValue::Date(i32::from_be_bytes(data.try_into()?))),
241        OID_TIME => Ok(DecodedValue::Time(i64::from_be_bytes(data.try_into()?))),
242        OID_TIMESTAMP => Ok(DecodedValue::Timestamp(i64::from_be_bytes(data.try_into()?))),
243        OID_TIMESTAMPTZ => Ok(DecodedValue::Timestamptz(i64::from_be_bytes(data.try_into()?))),
244        OID_JSONB => decode_jsonb(data),
245        OID_RECORD => decode_record(data, ext),
246        OID_RECORD_ARRAY => decode_array(data, ext),
247        OID_BOOL_ARRAY | OID_BYTEA_ARRAY | OID_INT2_ARRAY | OID_INT4_ARRAY | OID_INT8_ARRAY | OID_TEXT_ARRAY
248        | OID_BPCHAR_ARRAY | OID_VARCHAR_ARRAY | OID_FLOAT4_ARRAY | OID_FLOAT8_ARRAY | OID_NUMERIC_ARRAY
249        | OID_UUID_ARRAY | OID_JSONB_ARRAY => decode_array(data, ext),
250        OID_INT4RANGE | OID_INT8RANGE | OID_NUMRANGE | OID_TSRANGE | OID_TSTZRANGE | OID_DATERANGE => {
251            decode_range(data, range_element_oid(oid).expect("range OID"), ext)
252        }
253        OID_INT4MULTIRANGE | OID_INT8MULTIRANGE | OID_NUMMULTIRANGE | OID_TSMULTIRANGE | OID_TSTZMULTIRANGE
254        | OID_DATEMULTIRANGE => decode_multirange(data, range_element_oid(oid).expect("multirange OID"), ext),
255        // Everything past this point has a database-assigned OID, so it can
256        // only be resolved through the registry discovered at connect time.
257        _ if ext.enums.contains(&oid) => {
258            // An enum's binary representation is its label, as text.
259            Ok(DecodedValue::Str(std::str::from_utf8(data)?.to_string()))
260        }
261        // The element OID is in the array's own header, so this only has to
262        // recognise that the type is an array at all.
263        _ if ext.arrays.contains(&oid) => decode_array(data, ext),
264        _ => match ext.domains.get(&oid) {
265            // A domain is a constrained alias: same wire format as its base.
266            Some(&base_oid) => decode_value(base_oid, data, ext),
267            None => Err(Error::UnknownTypeOid { oid }),
268        },
269    }
270}
271
272fn decode_numeric(data: &[u8]) -> Result<DecodedValue> {
273    Ok(DecodedValue::Decimal(numeric::decode(data)?))
274}
275
276/// PostgreSQL's binary `interval` wire format: `i64 microseconds, i32 days,
277/// i32 months`, in that order — see `interval_send` in Postgres's own
278/// `timestamp.c`. Backs both `std::duration` and `cal::relative_duration`
279/// (see `DecodedValue::Interval`'s own doc comment for why `months` isn't
280/// folded into `days`).
281fn decode_interval(data: &[u8]) -> Result<DecodedValue> {
282    if data.len() != 16 {
283        return Err(Error::message(format!(
284            "malformed interval: expected 16 bytes, got {}",
285            data.len()
286        )));
287    }
288    let microseconds = i64::from_be_bytes(data[0..8].try_into()?);
289    let days = i32::from_be_bytes(data[8..12].try_into()?);
290    let months = i32::from_be_bytes(data[12..16].try_into()?);
291    Ok(DecodedValue::Interval {
292        months,
293        days,
294        microseconds,
295    })
296}
297
298// PostgreSQL's range binary-format flag bits (`rangetypes.h`).
299const RANGE_EMPTY: u8 = 0x01;
300const RANGE_LB_INC: u8 = 0x02;
301const RANGE_UB_INC: u8 = 0x04;
302const RANGE_LB_INF: u8 = 0x08;
303const RANGE_UB_INF: u8 = 0x10;
304
305/// Binary range: `u8 flags`, then — only when not empty — a length-prefixed
306/// lower bound (skipped if `RANGE_LB_INF`) and a length-prefixed upper bound
307/// (skipped if `RANGE_UB_INF`), each bound decoded with `element_oid`'s own
308/// decoder (see `range_element_oid` for which element type backs which
309/// range OID).
310fn decode_range(data: &[u8], element_oid: u32, ext: &ExtensionOids) -> Result<DecodedValue> {
311    let flags = data[0];
312    let mut offset = 1usize;
313    if flags & RANGE_EMPTY != 0 {
314        return Ok(DecodedValue::Range {
315            lower: None,
316            upper: None,
317            inc_lower: false,
318            inc_upper: false,
319            empty: true,
320        });
321    }
322    let lower = if flags & RANGE_LB_INF != 0 {
323        None
324    } else {
325        let len = i32::from_be_bytes(data[offset..offset + 4].try_into()?) as usize;
326        offset += 4;
327        let value = decode_value(element_oid, &data[offset..offset + len], ext)?;
328        offset += len;
329        Some(Box::new(value))
330    };
331    let upper = if flags & RANGE_UB_INF != 0 {
332        None
333    } else {
334        let len = i32::from_be_bytes(data[offset..offset + 4].try_into()?) as usize;
335        offset += 4;
336        Some(Box::new(decode_value(element_oid, &data[offset..offset + len], ext)?))
337    };
338    Ok(DecodedValue::Range {
339        lower,
340        upper,
341        inc_lower: flags & RANGE_LB_INC != 0,
342        inc_upper: flags & RANGE_UB_INC != 0,
343        empty: false,
344    })
345}
346
347/// Binary multirange: `i32 range_count`, then per range an `i32 len` +
348/// `len` bytes of that range's own binary encoding (the same format
349/// `decode_range` reads). Decodes to a plain `Array` of `Range` values —
350/// see `DecodedValue::Range`'s own doc comment for why there's no separate
351/// multirange variant.
352fn decode_multirange(data: &[u8], element_oid: u32, ext: &ExtensionOids) -> Result<DecodedValue> {
353    let mut offset = 0usize;
354    let count = i32::from_be_bytes(data[offset..offset + 4].try_into()?) as usize;
355    offset += 4;
356    let mut ranges = Vec::with_capacity(count);
357    for _ in 0..count {
358        let len = i32::from_be_bytes(data[offset..offset + 4].try_into()?) as usize;
359        offset += 4;
360        ranges.push(decode_range(&data[offset..offset + len], element_oid, ext)?);
361        offset += len;
362    }
363    Ok(DecodedValue::Array(ranges))
364}
365
366/// Binary jsonb: a 1-byte format-version prefix (always `1` today) followed
367/// by the UTF-8 JSON text itself. Parsed into a `DecodedValue` tree (not
368/// left as an opaque string) so nested jsonb-backed named tuples decode
369/// the same way a composite field would.
370fn decode_jsonb(data: &[u8]) -> Result<DecodedValue> {
371    let text = std::str::from_utf8(&data[1..])?;
372    let raw: &serde_json::value::RawValue = serde_json::from_str(text)?;
373    json_to_cached(raw)
374}
375
376/// jsonb, with every number's own digits intact.
377///
378/// `serde_json::Value` cannot carry them: it parses `12.3400` into an `f64`,
379/// which keeps neither the scale a money value is written in nor anything
380/// past float precision, and the text is gone before this sees the value at
381/// all. Walking `RawValue`s instead leaves each number as it was written
382/// (see `DecodedValue::JsonNumber`).
383fn json_to_cached(raw: &serde_json::value::RawValue) -> Result<DecodedValue> {
384    let text = raw.get().trim();
385    Ok(match text.as_bytes().first() {
386        None | Some(b'n') => DecodedValue::Null,
387        Some(b't') => DecodedValue::Bool(true),
388        Some(b'f') => DecodedValue::Bool(false),
389        Some(b'"') => DecodedValue::Str(serde_json::from_str(text)?),
390        Some(b'[') => {
391            let items: Vec<&serde_json::value::RawValue> = serde_json::from_str(text)?;
392            DecodedValue::Array(items.into_iter().map(json_to_cached).collect::<Result<Vec<_>>>()?)
393        }
394        Some(b'{') => {
395            // A `BTreeMap` keeps the key order this has always produced —
396            // `serde_json::Value::Object` is one too, when its own
397            // `preserve_order` feature is off.
398            let fields: std::collections::BTreeMap<String, &serde_json::value::RawValue> = serde_json::from_str(text)?;
399            DecodedValue::Object(
400                fields
401                    .into_iter()
402                    .map(|(key, value)| Ok((key, json_to_cached(value)?)))
403                    .collect::<Result<Vec<_>>>()?,
404            )
405        }
406        // An integer still reads as one, the way every jsonb integer always
407        // has; everything else keeps its digits for its reader to place.
408        _ => match text.parse::<i64>() {
409            Ok(integer) => DecodedValue::I64(integer),
410            Err(_) => DecodedValue::JsonNumber(text.to_string()),
411        },
412    })
413}
414
415/// `pgvector`'s binary format: `u16 ndim`, `u16 reserved` (always 0), then
416/// `ndim` big-endian `f32`s. Matches `_decode_vector_binary` exactly.
417fn decode_vector(data: &[u8]) -> Result<Vec<DecodedValue>> {
418    let ndim = u16::from_be_bytes(data[0..2].try_into()?) as usize;
419    let mut values = Vec::with_capacity(ndim);
420    for i in 0..ndim {
421        let start = 4 + i * 4;
422        let f = f32::from_be_bytes(data[start..start + 4].try_into()?);
423        values.push(DecodedValue::F64(f as f64));
424    }
425    Ok(values)
426}
427
428/// Encodes `items` (each expected to be `DecodedValue::F64`/`I64`) as
429/// pgvector's binary format — the inverse of `decode_vector`.
430fn encode_vector(items: &[DecodedValue], out: &mut bytes::BytesMut) -> Result<()> {
431    let ndim: u16 = items
432        .len()
433        .try_into()
434        .map_err(|_| Error::message("vector has too many dimensions to encode"))?;
435    out.put_u16(ndim);
436    out.put_u16(0); // reserved
437    for item in items {
438        let f = match item {
439            DecodedValue::F64(f) => *f as f32,
440            DecodedValue::I64(i) => *i as f32,
441            other => return Err(Error::message(format!("cannot encode {other:?} as a vector element"))),
442        };
443        out.put_f32(f);
444    }
445    Ok(())
446}
447
448/// Decodes a `record`-typed field: `i32 nfields`, then per field `u32
449/// type_oid` + `i32 field_len` (`-1` = NULL) + `field_len` bytes.
450fn decode_record(data: &[u8], ext: &ExtensionOids) -> Result<DecodedValue> {
451    let mut offset = 0usize;
452    let nfields = i32::from_be_bytes(data[offset..offset + 4].try_into()?) as usize;
453    offset += 4;
454    let mut fields = Vec::with_capacity(nfields);
455    for _ in 0..nfields {
456        let type_oid = u32::from_be_bytes(data[offset..offset + 4].try_into()?);
457        offset += 4;
458        let field_len = i32::from_be_bytes(data[offset..offset + 4].try_into()?);
459        offset += 4;
460        if field_len == -1 {
461            fields.push(DecodedValue::Null);
462        } else {
463            let len = field_len as usize;
464            fields.push(decode_value(type_oid, &data[offset..offset + len], ext)?);
465            offset += len;
466        }
467    }
468    Ok(DecodedValue::Composite(fields))
469}
470
471/// Decodes any 1-dimensional array: `i32 ndim`, `i32 has_null_flag`, `u32
472/// element_oid`, one `(i32 dim_size, i32 lower_bound)` pair, then per
473/// element `i32 len` (`-1` = NULL) + `len` bytes. An empty array
474/// (`ndim == 0`) has no dimension pair to read.
475fn decode_array(data: &[u8], ext: &ExtensionOids) -> Result<DecodedValue> {
476    let mut offset = 0usize;
477    let ndim = i32::from_be_bytes(data[offset..offset + 4].try_into()?);
478    offset += 4;
479    offset += 4; // has-null flag — not needed, NULL is signaled per-element via len == -1
480    let element_oid = u32::from_be_bytes(data[offset..offset + 4].try_into()?);
481    offset += 4;
482    if ndim == 0 {
483        return Ok(DecodedValue::Array(vec![]));
484    }
485    let dim_size = i32::from_be_bytes(data[offset..offset + 4].try_into()?) as usize;
486    offset += 4;
487    offset += 4; // lower bound — Pylon arrays are always 1-based, not needed
488
489    let mut items = Vec::with_capacity(dim_size);
490    for _ in 0..dim_size {
491        let elem_len = i32::from_be_bytes(data[offset..offset + 4].try_into()?);
492        offset += 4;
493        if elem_len == -1 {
494            items.push(DecodedValue::Null);
495        } else {
496            let len = elem_len as usize;
497            items.push(decode_value(element_oid, &data[offset..offset + len], ext)?);
498            offset += len;
499        }
500    }
501    Ok(DecodedValue::Array(items))
502}
503
504// ── Parameter encoding (the inverse direction: DecodedValue -> wire bytes) ──
505//
506// Bound query parameters don't need pylon-core to supply explicit
507// per-parameter Postgres types up front: `Client::prepare` already asks
508// Postgres itself to analyze the SQL and report each `$1, $2, ...`'s
509// expected `Type` back (`Statement::params()`) — which drives
510// own extended-query-protocol binding already relies on today, just
511// encoding explicitly here instead of hiding it inside a codec
512// registry. So encoding is *type-directed*: given a `DecodedValue` and the
513// `Type` Postgres reported for that position, write the matching binary
514// representation. See `BoundParam` (in `lib.rs`) for the `ToSql` glue that
515// makes this pluggable into `tokio_postgres::Client::query`.
516
517use bytes::BufMut;
518use postgres_types::{IsNull, Kind, Type};
519
520/// Whether a string's own bytes are `ty`'s binary wire format.
521///
522/// True for the text-like types and for anything Postgres transmits as text:
523/// an enum travels as its label (which is what `decode_value` reads back), and
524/// `json` -- unlike `jsonb`, which carries a version byte first -- is its
525/// document verbatim. `uuid`, `jsonb` and `numeric` accept a string too, but by
526/// conversion rather than by copying, so they are handled at their own arms.
527/// `bytea` is here because a string bound to it has always meant those bytes.
528fn accepts_text_bytes(ty: &Type) -> bool {
529    match ty.kind() {
530        Kind::Enum(_) => true,
531        // A domain is a constrained alias, so it takes whatever its base does.
532        Kind::Domain(base) => accepts_text_bytes(base),
533        _ => matches!(
534            *ty,
535            Type::TEXT | Type::VARCHAR | Type::BPCHAR | Type::NAME | Type::JSON | Type::BYTEA | Type::UNKNOWN
536        ),
537    }
538}
539
540/// Encodes `value` as `ty`'s binary wire format into `out`. `ty` comes from
541/// `Statement::params()[i]` — Postgres's own analysis of the prepared SQL,
542/// not a guess — so this only needs to pick the right byte width/shape for
543/// whatever `DecodedValue` variant is actually being sent, not infer the
544/// target type itself.
545pub fn encode_value(value: &DecodedValue, ty: &Type, out: &mut bytes::BytesMut) -> Result<IsNull> {
546    let DecodedValue::Null = value else {
547        return encode_non_null(value, ty, out);
548    };
549    Ok(IsNull::Yes)
550}
551
552fn encode_non_null(value: &DecodedValue, ty: &Type, out: &mut bytes::BytesMut) -> Result<IsNull> {
553    // A `<std::decimal>$pN` cast makes Postgres report that parameter's
554    // type as `numeric` regardless of which `DecodedValue` variant the JSON
555    // request body produced (`I64`/`F64` for a JSON number, `Str` for a
556    // JSON string — `json_to_cached_value` in pylon-server has no visibility
557    // into the target PG type at parse time). Without this, the arms below
558    // write raw int8/float8 bytes or raw UTF-8 text straight into a
559    // numeric-typed slot, which Postgres's binary numeric decoder then reads
560    // as a corrupt header — "invalid sign in external representation" for
561    // I64/F64 (garbage sign field), "insufficient data left in message" for
562    // Str (too few bytes for the header). Route every numeric-ish variant
563    // through the same `numeric` encoding the `Decimal` arm below uses.
564    if *ty == Type::NUMERIC {
565        let text = match value {
566            DecodedValue::Decimal(s) | DecodedValue::Str(s) => s.clone(),
567            DecodedValue::I64(i) => i.to_string(),
568            // `{f}` is the shortest form that reads back as the same f64, so
569            // 0.1 binds as `0.1` rather than the full binary expansion.
570            DecodedValue::F64(f) => format!("{f}"),
571            _ => return Err(Error::message("cannot bind this value as a numeric parameter")),
572        };
573        numeric::encode(&text, out)?;
574        return Ok(IsNull::No);
575    }
576    match value {
577        DecodedValue::Null => unreachable!("caller already handled NULL"),
578        DecodedValue::Bool(b) => out.put_u8(*b as u8),
579        DecodedValue::I64(i) => {
580            if *ty == Type::INT2 {
581                out.put_i16(*i as i16);
582            } else if *ty == Type::INT4 {
583                out.put_i32(*i as i32);
584            } else {
585                out.put_i64(*i);
586            }
587        }
588        DecodedValue::F64(f) => {
589            if *ty == Type::FLOAT4 {
590                out.put_f32(*f as f32);
591            } else {
592                out.put_f64(*f);
593            }
594        }
595        DecodedValue::Str(s) => {
596            if *ty == Type::UUID {
597                // A JSON API request body necessarily carries a UUID query
598                // parameter as plain text (there's no JSON "uuid" type), so
599                // it arrives here as a `DecodedValue::Str`, not `::Uuid` —
600                // a `uuid` codec conventionally accepts a plain string the
601                // same way. Without this, the raw UTF-8 text bytes get sent
602                // for a binary-format `uuid` parameter, which Postgres
603                // rejects with "incorrect binary data format".
604                out.put_slice(&parse_uuid_str(s)?);
605            } else if *ty == Type::JSONB {
606                // A caller that already has serialized JSON text (e.g.
607                // `schema_to_db_state_json`'s output, bound as `$1::jsonb`
608                // in `migration apply`'s db_state snapshot update) arrives
609                // here as `DecodedValue::Str`, not `::Object` — treat it as
610                // already-valid JSON text and just add jsonb's binary
611                // version-byte prefix (see `decode_jsonb`/the `Object` arm
612                // below), rather than writing raw text bytes with no
613                // framing, which Postgres would reject.
614                out.put_u8(1);
615                out.put_slice(s.as_bytes());
616            } else if accepts_text_bytes(ty) {
617                out.put_slice(s.as_bytes());
618            } else {
619                // Without this the raw UTF-8 went into a binary-format slot of
620                // whatever type the parameter actually has, and Postgres reported
621                // its own reading of the bytes -- "insufficient data left in
622                // message" for an int8 or interval, "incorrect binary data
623                // format" for a bool or timestamptz. That came from the server,
624                // so it also left an open transaction aborted; refusing here
625                // names the type that was wanted and sends nothing.
626                return Err(Error::message(format!(
627                    "cannot bind a string as a parameter of type {:?}",
628                    ty.name()
629                )));
630            }
631        }
632        DecodedValue::Bytes(b) => out.put_slice(b),
633        DecodedValue::Uuid(bytes) => out.put_slice(bytes),
634        DecodedValue::Decimal(s) => numeric::encode(s, out)?,
635        DecodedValue::Array(items) if ty.name() == "vector" => {
636            // `$n::vector` casts the parameter directly (unlike
637            // `vector::search`'s `$n::float8[]::vector`, where the *inner*
638            // cast is what Postgres's prepare step reports as the param's
639            // type) — Postgres reports `$n` itself as `vector`, a scalar
640            // extension type, not `Kind::Array`. Bypass the generic array
641            // path entirely and write pgvector's own binary format.
642            encode_vector(items, out)?;
643        }
644        DecodedValue::Array(items) => {
645            let element_ty = match ty.kind() {
646                Kind::Array(inner) => inner.clone(),
647                // Not actually an array type per Postgres's own analysis —
648                // fall back to TEXT so encoding still proceeds deterministically
649                // rather than panicking; a real mismatch surfaces as a
650                // Postgres-side type error on execute, same as today.
651                _ => Type::TEXT,
652            };
653            encode_array(items, &element_ty, out)?;
654        }
655        DecodedValue::Composite(items) if *ty == Type::JSONB => {
656            // An all-unnamed `tuple<str, bool>` is a jsonb *array* — the
657            // positional form `_decode`'s named-tuple case reads back by
658            // index. JSON needs no per-field Postgres type, which is what
659            // makes this encodable where the composite below is not.
660            out.put_u8(1); // jsonb binary format version prefix
661            let mut json = String::new();
662            write_cached_json(value, &mut json);
663            out.put_slice(json.as_bytes());
664        }
665        DecodedValue::Composite(_) => {
666            // Outside jsonb, a composite only arises from *decoding* a query
667            // result (see `decode_record`): PyQL binds no raw record as a
668            // parameter, and encoding one correctly would need a per-field
669            // Postgres type that isn't available here (only the original
670            // compiled query's shape carries that). Erroring is safer than
671            // guessing wrong field types.
672            return Err(Error::message("cannot bind a composite value as a query parameter"));
673        }
674        DecodedValue::Object(fields) => {
675            out.put_u8(1); // jsonb binary format version prefix
676            let mut json = String::new();
677            write_cached_object_json(fields, &mut json);
678            out.put_slice(json.as_bytes());
679        }
680        // Its own digits, wherever they are going: a jsonb number keeps them
681        // (that is why it is carried as text at all), `numeric` parses them
682        // exactly, and a text column takes them as written.
683        DecodedValue::JsonNumber(digits) if *ty == Type::JSONB => {
684            out.put_u8(1);
685            out.put_slice(digits.as_bytes());
686        }
687        DecodedValue::JsonNumber(digits) if *ty == Type::NUMERIC => numeric::encode(digits, out)?,
688        DecodedValue::JsonNumber(digits) if accepts_text_bytes(ty) => out.put_slice(digits.as_bytes()),
689        DecodedValue::JsonNumber(digits) => match digits.parse::<f64>() {
690            Ok(number) => encode_value(&DecodedValue::F64(number), ty, out).map(|_| ())?,
691            Err(_) => {
692                return Err(Error::message(format!(
693                    "cannot bind the JSON number {digits} as a parameter of type {:?}",
694                    ty.name()
695                )));
696            }
697        },
698        DecodedValue::Interval {
699            months,
700            days,
701            microseconds,
702        } => {
703            // Same field order as `decode_interval`'s read.
704            out.put_i64(*microseconds);
705            out.put_i32(*days);
706            out.put_i32(*months);
707        }
708        DecodedValue::Date(days) => out.put_i32(*days),
709        DecodedValue::Time(us) => out.put_i64(*us),
710        DecodedValue::Timestamp(us) => out.put_i64(*us),
711        DecodedValue::Timestamptz(us) => out.put_i64(*us),
712        DecodedValue::Range {
713            lower,
714            upper,
715            inc_lower,
716            inc_upper,
717            empty,
718        } => {
719            if *empty {
720                out.put_u8(RANGE_EMPTY);
721                return Ok(IsNull::No);
722            }
723            let element_ty = match ty.kind() {
724                Kind::Range(inner) => inner.clone(),
725                // Not actually a range type per Postgres's own analysis —
726                // fall back to TEXT so encoding proceeds deterministically;
727                // a real mismatch surfaces as a Postgres-side error, same
728                // as the analogous fallback in the `Array` arm above.
729                _ => Type::TEXT,
730            };
731            let mut flags = 0u8;
732            if *inc_lower {
733                flags |= RANGE_LB_INC;
734            }
735            if *inc_upper {
736                flags |= RANGE_UB_INC;
737            }
738            if lower.is_none() {
739                flags |= RANGE_LB_INF;
740            }
741            if upper.is_none() {
742                flags |= RANGE_UB_INF;
743            }
744            out.put_u8(flags);
745            for bound in [lower, upper].into_iter().flatten() {
746                let mut buf = bytes::BytesMut::new();
747                encode_value(bound, &element_ty, &mut buf)?;
748                out.put_i32(buf.len() as i32);
749                out.put_slice(&buf);
750            }
751        }
752    }
753    Ok(IsNull::No)
754}
755
756fn write_cached_object_json(fields: &[(String, DecodedValue)], out: &mut String) {
757    out.push('{');
758    for (index, (key, value)) in fields.iter().enumerate() {
759        if index > 0 {
760            out.push(',');
761        }
762        out.push_str(&serde_json::Value::String(key.clone()).to_string());
763        out.push(':');
764        write_cached_json(value, out);
765    }
766    out.push('}');
767}
768
769/// A value as jsonb text. Written rather than built as a
770/// `serde_json::Value` for one reason: a number that kept its own digits
771/// (`DecodedValue::JsonNumber`) cannot be held by one without rounding it
772/// back through an `f64` — see `json_to_cached`.
773fn write_cached_json(value: &DecodedValue, out: &mut String) {
774    match value {
775        DecodedValue::JsonNumber(digits) => out.push_str(digits),
776        DecodedValue::Object(fields) => write_cached_object_json(fields, out),
777        DecodedValue::Array(items) | DecodedValue::Composite(items) => {
778            out.push('[');
779            for (index, item) in items.iter().enumerate() {
780                if index > 0 {
781                    out.push(',');
782                }
783                write_cached_json(item, out);
784            }
785            out.push(']');
786        }
787        other => out.push_str(&cached_to_json(other).to_string()),
788    }
789}
790
791fn cached_to_json(value: &DecodedValue) -> serde_json::Value {
792    match value {
793        DecodedValue::Null => serde_json::Value::Null,
794        DecodedValue::Bool(b) => serde_json::Value::Bool(*b),
795        DecodedValue::I64(i) => serde_json::Value::Number((*i).into()),
796        DecodedValue::F64(f) => serde_json::Number::from_f64(*f)
797            .map(serde_json::Value::Number)
798            .unwrap_or(serde_json::Value::Null),
799        DecodedValue::Str(s) => serde_json::Value::String(s.clone()),
800        DecodedValue::Bytes(b) => serde_json::Value::String(hex::encode(b)),
801        DecodedValue::Uuid(bytes) => serde_json::Value::String(format_uuid(bytes)),
802        DecodedValue::Decimal(s) => serde_json::Value::String(s.clone()),
803        DecodedValue::Array(items) | DecodedValue::Composite(items) => {
804            serde_json::Value::Array(items.iter().map(cached_to_json).collect())
805        }
806        DecodedValue::Object(fields) => {
807            serde_json::Value::Object(fields.iter().map(|(k, v)| (k.clone(), cached_to_json(v))).collect())
808        }
809        // Only reachable for a value nested inside one of the containers
810        // `cached_to_json` still answers for; `write_cached_json` takes the
811        // lossless path for every container that can hold one.
812        DecodedValue::JsonNumber(digits) => digits
813            .parse::<serde_json::Number>()
814            .map(serde_json::Value::Number)
815            .unwrap_or(serde_json::Value::Null),
816        // No natural JSON scalar for an interval; only reachable if an
817        // Interval value ends up nested inside an Object being sent as a
818        // jsonb parameter — represented as its raw components so it's at
819        // least round-trippable, not silently dropped.
820        DecodedValue::Interval {
821            months,
822            days,
823            microseconds,
824        } => serde_json::json!({
825            "months": months, "days": days, "microseconds": microseconds,
826        }),
827        // Same rationale as Interval above — raw PG wire units, not a
828        // formatted calendar string (calendar math is deliberately left to
829        // Python's own `datetime` module at the `pgvalue.rs` boundary, not
830        // reimplemented here).
831        DecodedValue::Date(days) => serde_json::json!({ "days_since_2000_01_01": days }),
832        DecodedValue::Time(us) => serde_json::json!({ "microseconds_since_midnight": us }),
833        DecodedValue::Timestamp(us) => serde_json::json!({ "microseconds_since_2000_01_01": us }),
834        DecodedValue::Timestamptz(us) => serde_json::json!({ "microseconds_since_2000_01_01_utc": us }),
835        DecodedValue::Range {
836            lower,
837            upper,
838            inc_lower,
839            inc_upper,
840            empty,
841        } => serde_json::json!({
842            "lower": lower.as_deref().map(cached_to_json),
843            "upper": upper.as_deref().map(cached_to_json),
844            "inc_lower": inc_lower,
845            "inc_upper": inc_upper,
846            "empty": empty,
847        }),
848    }
849}
850
851fn format_uuid(bytes: &[u8; 16]) -> String {
852    let hex = hex::encode(bytes);
853    format!(
854        "{}-{}-{}-{}-{}",
855        &hex[0..8],
856        &hex[8..12],
857        &hex[12..16],
858        &hex[16..20],
859        &hex[20..32]
860    )
861}
862
863/// Parses a hyphenated UUID string into its 16 raw bytes — the inverse of
864/// `format_uuid`. Tolerates the hyphens being anywhere/absent (just strips
865/// every `-` and hex-decodes what's left) rather than validating the exact
866/// `8-4-4-4-12` grouping, since the only thing that matters here is
867/// recovering the right 16 bytes, not rejecting non-canonical formatting.
868fn parse_uuid_str(s: &str) -> Result<[u8; 16]> {
869    let hex_only: String = s.chars().filter(|c| *c != '-').collect();
870    let bytes = hex::decode(&hex_only).map_err(|_| Error::message(format!("invalid UUID string: {s:?}")))?;
871    bytes
872        .try_into()
873        .map_err(|_: Vec<u8>| Error::message(format!("invalid UUID string: {s:?}")))
874}
875
876/// 1-dimensional Postgres array binary format (see the module doc comment
877/// for the layout) — the encode-side mirror of `decode_array`.
878fn encode_array(items: &[DecodedValue], element_ty: &Type, out: &mut bytes::BytesMut) -> Result<()> {
879    if items.is_empty() {
880        out.put_i32(0); // ndim
881        out.put_i32(0); // has-null flag
882        out.put_u32(element_ty.oid());
883        return Ok(());
884    }
885    let has_null = items.iter().any(|v| matches!(v, DecodedValue::Null));
886    out.put_i32(1); // ndim — Pylon arrays are always 1-D
887    out.put_i32(has_null as i32);
888    out.put_u32(element_ty.oid());
889    out.put_i32(items.len() as i32); // dim size
890    out.put_i32(1); // lower bound
891
892    for item in items {
893        if matches!(item, DecodedValue::Null) {
894            out.put_i32(-1);
895            continue;
896        }
897        let start = out.len();
898        out.put_i32(0); // placeholder length, patched below
899        let is_null = encode_value(item, element_ty, out)?;
900        let len = (out.len() - start - 4) as i32;
901        let len = if matches!(is_null, IsNull::Yes) { -1 } else { len };
902        out[start..start + 4].copy_from_slice(&len.to_be_bytes());
903    }
904    Ok(())
905}
906
907#[cfg(test)]
908mod tests {
909    use super::*;
910
911    fn no_ext() -> ExtensionOids {
912        ExtensionOids::default()
913    }
914
915    #[test]
916    fn decodes_bool() {
917        assert_eq!(
918            decode_value(OID_BOOL, &[1], &no_ext()).unwrap(),
919            DecodedValue::Bool(true)
920        );
921        assert_eq!(
922            decode_value(OID_BOOL, &[0], &no_ext()).unwrap(),
923            DecodedValue::Bool(false)
924        );
925    }
926
927    #[test]
928    fn decodes_integers() {
929        assert_eq!(
930            decode_value(OID_INT2, &7i16.to_be_bytes(), &no_ext()).unwrap(),
931            DecodedValue::I64(7)
932        );
933        assert_eq!(
934            decode_value(OID_INT4, &(-42i32).to_be_bytes(), &no_ext()).unwrap(),
935            DecodedValue::I64(-42)
936        );
937        assert_eq!(
938            decode_value(OID_INT8, &9_223_372_036_854_775_807i64.to_be_bytes(), &no_ext()).unwrap(),
939            DecodedValue::I64(9_223_372_036_854_775_807)
940        );
941    }
942
943    #[test]
944    fn decodes_floats() {
945        assert_eq!(
946            decode_value(OID_FLOAT4, &1.5f32.to_be_bytes(), &no_ext()).unwrap(),
947            DecodedValue::F64(1.5)
948        );
949        assert_eq!(
950            decode_value(OID_FLOAT8, &2.25f64.to_be_bytes(), &no_ext()).unwrap(),
951            DecodedValue::F64(2.25)
952        );
953    }
954
955    #[test]
956    fn decodes_text_varchar_bpchar() {
957        for oid in [OID_TEXT, OID_VARCHAR, OID_BPCHAR] {
958            assert_eq!(
959                decode_value(oid, "hello".as_bytes(), &no_ext()).unwrap(),
960                DecodedValue::Str("hello".to_string())
961            );
962        }
963    }
964
965    #[test]
966    fn decodes_unicode_text() {
967        assert_eq!(
968            decode_value(OID_TEXT, "héllo wörld 🎉".as_bytes(), &no_ext()).unwrap(),
969            DecodedValue::Str("héllo wörld 🎉".to_string())
970        );
971    }
972
973    #[test]
974    fn decodes_uuid() {
975        let bytes: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
976        assert_eq!(
977            decode_value(OID_UUID, &bytes, &no_ext()).unwrap(),
978            DecodedValue::Uuid(bytes)
979        );
980    }
981
982    #[test]
983    fn decodes_bytea() {
984        assert_eq!(
985            decode_value(OID_BYTEA, &[1, 2, 3, 255], &no_ext()).unwrap(),
986            DecodedValue::Bytes(vec![1, 2, 3, 255])
987        );
988    }
989
990    #[test]
991    fn decodes_interval() {
992        // Regression: interval has no dedicated binary decoder — it used to
993        // fall through to the UTF-8-text fallback, which panics/errors on
994        // interval's actual binary payload (microseconds/days/months, not text).
995        let mut data = Vec::new();
996        data.extend_from_slice(&3_600_000_000i64.to_be_bytes()); // 1 hour, in microseconds
997        data.extend_from_slice(&2i32.to_be_bytes()); // 2 days
998        data.extend_from_slice(&1i32.to_be_bytes()); // 1 month
999        assert_eq!(
1000            decode_value(OID_INTERVAL, &data, &no_ext()).unwrap(),
1001            DecodedValue::Interval {
1002                months: 1,
1003                days: 2,
1004                microseconds: 3_600_000_000
1005            }
1006        );
1007    }
1008
1009    #[test]
1010    fn encodes_interval() {
1011        let value = DecodedValue::Interval {
1012            months: 1,
1013            days: 2,
1014            microseconds: 3_600_000_000,
1015        };
1016        let mut out = bytes::BytesMut::new();
1017        encode_value(&value, &postgres_types::Type::INTERVAL, &mut out).unwrap();
1018        assert_eq!(decode_value(OID_INTERVAL, &out, &no_ext()).unwrap(), value);
1019    }
1020
1021    #[test]
1022    fn decodes_date_time_timestamp_timestamptz() {
1023        // Regression: these had no binary decoder either — date silently
1024        // returned garbage bytes (never even errored), the others panicked
1025        // on the same UTF-8-text-fallback assumption interval did.
1026        assert_eq!(
1027            decode_value(OID_DATE, &9525i32.to_be_bytes(), &no_ext()).unwrap(),
1028            DecodedValue::Date(9525)
1029        );
1030        assert_eq!(
1031            decode_value(OID_TIME, &3_600_000_000i64.to_be_bytes(), &no_ext()).unwrap(),
1032            DecodedValue::Time(3_600_000_000)
1033        );
1034        assert_eq!(
1035            decode_value(OID_TIMESTAMP, &1_000_000_000i64.to_be_bytes(), &no_ext()).unwrap(),
1036            DecodedValue::Timestamp(1_000_000_000)
1037        );
1038        assert_eq!(
1039            decode_value(OID_TIMESTAMPTZ, &1_000_000_000i64.to_be_bytes(), &no_ext()).unwrap(),
1040            DecodedValue::Timestamptz(1_000_000_000)
1041        );
1042    }
1043
1044    #[test]
1045    fn encodes_date_time_timestamp_timestamptz() {
1046        for (value, ty) in [
1047            (DecodedValue::Date(9525), postgres_types::Type::DATE),
1048            (DecodedValue::Time(3_600_000_000), postgres_types::Type::TIME),
1049            (DecodedValue::Timestamp(1_000_000_000), postgres_types::Type::TIMESTAMP),
1050            (
1051                DecodedValue::Timestamptz(1_000_000_000),
1052                postgres_types::Type::TIMESTAMPTZ,
1053            ),
1054        ] {
1055            let mut out = bytes::BytesMut::new();
1056            encode_value(&value, &ty, &mut out).unwrap();
1057            let oid = match &value {
1058                DecodedValue::Date(_) => OID_DATE,
1059                DecodedValue::Time(_) => OID_TIME,
1060                DecodedValue::Timestamp(_) => OID_TIMESTAMP,
1061                DecodedValue::Timestamptz(_) => OID_TIMESTAMPTZ,
1062                _ => unreachable!(),
1063            };
1064            assert_eq!(decode_value(oid, &out, &no_ext()).unwrap(), value);
1065        }
1066    }
1067
1068    #[test]
1069    fn decodes_a_bounded_int8range() {
1070        // flags = LB_INC | UB_INC-off = 0x02 (inclusive lower, exclusive upper)
1071        let mut data = vec![RANGE_LB_INC];
1072        data.extend_from_slice(&8i32.to_be_bytes());
1073        data.extend_from_slice(&1i64.to_be_bytes());
1074        data.extend_from_slice(&8i32.to_be_bytes());
1075        data.extend_from_slice(&10i64.to_be_bytes());
1076        assert_eq!(
1077            decode_value(OID_INT8RANGE, &data, &no_ext()).unwrap(),
1078            DecodedValue::Range {
1079                lower: Some(Box::new(DecodedValue::I64(1))),
1080                upper: Some(Box::new(DecodedValue::I64(10))),
1081                inc_lower: true,
1082                inc_upper: false,
1083                empty: false,
1084            }
1085        );
1086    }
1087
1088    #[test]
1089    fn decodes_an_empty_range() {
1090        assert_eq!(
1091            decode_value(OID_INT8RANGE, &[RANGE_EMPTY], &no_ext()).unwrap(),
1092            DecodedValue::Range {
1093                lower: None,
1094                upper: None,
1095                inc_lower: false,
1096                inc_upper: false,
1097                empty: true
1098            }
1099        );
1100    }
1101
1102    #[test]
1103    fn decodes_an_unbounded_range() {
1104        // Both bounds infinite: flags = LB_INF | UB_INF, no bound payloads follow.
1105        let data = [RANGE_LB_INF | RANGE_UB_INF];
1106        assert_eq!(
1107            decode_value(OID_INT8RANGE, &data, &no_ext()).unwrap(),
1108            DecodedValue::Range {
1109                lower: None,
1110                upper: None,
1111                inc_lower: false,
1112                inc_upper: false,
1113                empty: false
1114            }
1115        );
1116    }
1117
1118    #[test]
1119    fn encodes_and_round_trips_an_int8range() {
1120        let value = DecodedValue::Range {
1121            lower: Some(Box::new(DecodedValue::I64(1))),
1122            upper: Some(Box::new(DecodedValue::I64(10))),
1123            inc_lower: true,
1124            inc_upper: false,
1125            empty: false,
1126        };
1127        let mut out = bytes::BytesMut::new();
1128        encode_value(&value, &postgres_types::Type::INT8_RANGE, &mut out).unwrap();
1129        assert_eq!(decode_value(OID_INT8RANGE, &out, &no_ext()).unwrap(), value);
1130    }
1131
1132    #[test]
1133    fn decodes_a_multirange_of_int8ranges() {
1134        let mut range1 = vec![RANGE_LB_INC];
1135        range1.extend_from_slice(&8i32.to_be_bytes());
1136        range1.extend_from_slice(&1i64.to_be_bytes());
1137        range1.extend_from_slice(&8i32.to_be_bytes());
1138        range1.extend_from_slice(&3i64.to_be_bytes());
1139
1140        let mut range2 = vec![RANGE_LB_INC];
1141        range2.extend_from_slice(&8i32.to_be_bytes());
1142        range2.extend_from_slice(&5i64.to_be_bytes());
1143        range2.extend_from_slice(&8i32.to_be_bytes());
1144        range2.extend_from_slice(&7i64.to_be_bytes());
1145
1146        let mut data = 2i32.to_be_bytes().to_vec();
1147        data.extend_from_slice(&(range1.len() as i32).to_be_bytes());
1148        data.extend_from_slice(&range1);
1149        data.extend_from_slice(&(range2.len() as i32).to_be_bytes());
1150        data.extend_from_slice(&range2);
1151
1152        let decoded = decode_value(OID_INT8MULTIRANGE, &data, &no_ext()).unwrap();
1153        assert_eq!(
1154            decoded,
1155            DecodedValue::Array(vec![
1156                DecodedValue::Range {
1157                    lower: Some(Box::new(DecodedValue::I64(1))),
1158                    upper: Some(Box::new(DecodedValue::I64(3))),
1159                    inc_lower: true,
1160                    inc_upper: false,
1161                    empty: false,
1162                },
1163                DecodedValue::Range {
1164                    lower: Some(Box::new(DecodedValue::I64(5))),
1165                    upper: Some(Box::new(DecodedValue::I64(7))),
1166                    inc_lower: true,
1167                    inc_upper: false,
1168                    empty: false,
1169                },
1170            ])
1171        );
1172    }
1173
1174    #[test]
1175    fn errors_on_an_oid_no_rule_or_discovery_covers() {
1176        // The old behaviour here was to decode the raw binary as UTF-8 text,
1177        // which silently produced mojibake for every non-text type whose OID
1178        // isn't a builtin (`vector` above all). An unclassified OID is now an
1179        // error rather than a guess.
1180        let err = decode_value(999_999, &[0xff, 0xfe], &no_ext()).unwrap_err();
1181        assert!(matches!(err, Error::UnknownTypeOid { oid: 999_999 }), "got {err:?}");
1182    }
1183
1184    #[test]
1185    fn decodes_a_discovered_enum_oid_as_its_label_text() {
1186        let ext = ExtensionOids {
1187            enums: std::collections::HashSet::from([50_001]),
1188            ..Default::default()
1189        };
1190        assert_eq!(
1191            decode_value(50_001, "Active".as_bytes(), &ext).unwrap(),
1192            DecodedValue::Str("Active".to_string())
1193        );
1194    }
1195
1196    #[test]
1197    fn decodes_a_discovered_domain_through_its_base_type() {
1198        // A domain over int8 has to decode as int8, not as text — the old
1199        // fallback would have run UTF-8 validation over these eight bytes.
1200        let ext = ExtensionOids {
1201            domains: std::collections::HashMap::from([(50_002, OID_INT8)]),
1202            ..Default::default()
1203        };
1204        assert_eq!(
1205            decode_value(50_002, &7i64.to_be_bytes(), &ext).unwrap(),
1206            DecodedValue::I64(7)
1207        );
1208    }
1209
1210    #[test]
1211    fn discovery_rows_populate_vector_enums_and_domains() {
1212        let ext = ExtensionOids::from_discovery_rows([
1213            (50_000, "b".to_string(), 0, "vector".to_string()),
1214            (50_001, "e".to_string(), 0, "status".to_string()),
1215            (50_002, "d".to_string(), OID_INT8, "positive_int".to_string()),
1216            // A domain with no resolvable base is skipped rather than
1217            // recorded as pointing at OID 0.
1218            (50_003, "d".to_string(), 0, "broken".to_string()),
1219        ]);
1220        assert_eq!(ext.vector, Some(50_000));
1221        assert!(ext.enums.contains(&50_001));
1222        assert_eq!(ext.domains.get(&50_002), Some(&OID_INT8));
1223        assert!(!ext.domains.contains_key(&50_003));
1224    }
1225
1226    #[test]
1227    fn discovery_rows_record_array_types() {
1228        let ext = ExtensionOids::from_discovery_rows([
1229            (50_001, "e".to_string(), 0, "status".to_string()),
1230            (50_010, "A".to_string(), 50_001, "_status".to_string()),
1231        ]);
1232        assert!(ext.enums.contains(&50_001));
1233        assert!(ext.arrays.contains(&50_010));
1234    }
1235
1236    #[test]
1237    fn decodes_an_array_of_a_discovered_enum() {
1238        // An `AuthenticationMethod[]` column: the array's own OID is
1239        // database-assigned, so without discovery it is an UnknownTypeOid
1240        // even though the element OID travels in the array header and every
1241        // label in it decodes.
1242        let ext = ExtensionOids {
1243            enums: std::collections::HashSet::from([50_001]),
1244            arrays: std::collections::HashSet::from([50_010]),
1245            ..Default::default()
1246        };
1247        let encoded = encode_array(50_001, &[Some(b"Password"), Some(b"Passkey")]);
1248        assert_eq!(
1249            decode_value(50_010, &encoded, &ext).unwrap(),
1250            DecodedValue::Array(vec![
1251                DecodedValue::Str("Password".to_string()),
1252                DecodedValue::Str("Passkey".to_string()),
1253            ])
1254        );
1255    }
1256
1257    #[test]
1258    fn an_array_of_an_undiscovered_type_is_still_an_error() {
1259        let ext = ExtensionOids::default();
1260        let encoded = encode_array(50_001, &[Some(b"Password")]);
1261        let err = decode_value(50_010, &encoded, &ext).unwrap_err();
1262        assert!(matches!(err, Error::UnknownTypeOid { oid: 50_010 }), "got {err:?}");
1263    }
1264
1265    #[test]
1266    fn a_vector_inside_a_record_decodes_when_discovery_ran() {
1267        // The real C-4 shape: pylon-core wraps every result in `SELECT (...)
1268        // AS result`, so a vector value arrives nested in a record and is
1269        // decoded through `decode_record`'s own per-field OID dispatch.
1270        let mut vec_bytes = 2u16.to_be_bytes().to_vec();
1271        vec_bytes.extend_from_slice(&0u16.to_be_bytes());
1272        vec_bytes.extend_from_slice(&1.5f32.to_be_bytes());
1273        vec_bytes.extend_from_slice(&2.5f32.to_be_bytes());
1274        let rec = encode_record(&[(OID_TEXT, Some(b"doc")), (50_000, Some(&vec_bytes))]);
1275
1276        let ext = ExtensionOids {
1277            vector: Some(50_000),
1278            ..Default::default()
1279        };
1280        let decoded = decode_value(OID_RECORD, &rec, &ext).unwrap();
1281        let DecodedValue::Composite(fields) = decoded else {
1282            panic!("expected Composite, got {decoded:?}")
1283        };
1284        assert_eq!(fields[0], DecodedValue::Str("doc".to_string()));
1285        assert_eq!(
1286            fields[1],
1287            DecodedValue::Array(vec![DecodedValue::F64(1.5), DecodedValue::F64(2.5)])
1288        );
1289
1290        // Without discovery the same bytes must fail loudly, not silently.
1291        let err = decode_value(OID_RECORD, &rec, &no_ext()).unwrap_err();
1292        assert!(matches!(err, Error::UnknownTypeOid { oid: 50_000 }), "got {err:?}");
1293    }
1294
1295    /// Builds the Postgres binary `numeric` wire format by hand: `u16
1296    /// ndigits`, `i16 weight`, `u16 sign`, `i16 dscale`, then `ndigits`
1297    /// base-10000 digit groups (each a `u16`, matching `NBASE = 10000`).
1298    fn encode_numeric(sign: u16, weight: i16, dscale: i16, digits: &[u16]) -> Vec<u8> {
1299        let mut buf = Vec::new();
1300        buf.extend_from_slice(&(digits.len() as u16).to_be_bytes());
1301        buf.extend_from_slice(&weight.to_be_bytes());
1302        buf.extend_from_slice(&sign.to_be_bytes());
1303        buf.extend_from_slice(&dscale.to_be_bytes());
1304        for d in digits {
1305            buf.extend_from_slice(&d.to_be_bytes());
1306        }
1307        buf
1308    }
1309
1310    #[test]
1311    fn decodes_numeric_integer() {
1312        // 12345 = digit groups [1, 2345] at weight 1 (10000^1 * 1 + 10000^0 * 2345)
1313        let data = encode_numeric(0x0000, 1, 0, &[1, 2345]);
1314        assert_eq!(
1315            decode_value(OID_NUMERIC, &data, &no_ext()).unwrap(),
1316            DecodedValue::Decimal("12345".to_string())
1317        );
1318    }
1319
1320    #[test]
1321    fn decodes_numeric_with_fraction() {
1322        // 12.50, dscale=2: digit groups [12, 5000] at weight 0
1323        let data = encode_numeric(0x0000, 0, 2, &[12, 5000]);
1324        assert_eq!(
1325            decode_value(OID_NUMERIC, &data, &no_ext()).unwrap(),
1326            DecodedValue::Decimal("12.50".to_string())
1327        );
1328    }
1329
1330    #[test]
1331    fn decodes_negative_numeric() {
1332        let data = encode_numeric(0x4000, 0, 2, &[12, 5000]);
1333        assert_eq!(
1334            decode_value(OID_NUMERIC, &data, &no_ext()).unwrap(),
1335            DecodedValue::Decimal("-12.50".to_string())
1336        );
1337    }
1338
1339    #[test]
1340    fn refuses_a_string_for_a_parameter_whose_binary_form_is_not_text() {
1341        // The raw UTF-8 used to go straight into the binary slot, leaving
1342        // Postgres to report its own reading of the bytes -- and aborting the
1343        // caller's transaction, because the complaint came from the server.
1344        for ty in [
1345            Type::INT8,
1346            Type::INT4,
1347            Type::BOOL,
1348            Type::INTERVAL,
1349            Type::TIMESTAMPTZ,
1350            Type::DATE,
1351        ] {
1352            let mut buffer = bytes::BytesMut::new();
1353            let Err(err) = encode_value(&DecodedValue::Str("25 days".into()), &ty, &mut buffer) else {
1354                panic!("{} must refuse a string", ty.name());
1355            };
1356            assert!(
1357                err.to_string().contains(ty.name()),
1358                "the message must name the type that was wanted: {err}"
1359            );
1360            assert!(buffer.is_empty(), "nothing may be written for a refused parameter");
1361        }
1362    }
1363
1364    #[test]
1365    fn a_string_still_reaches_the_types_it_is_the_wire_form_of() {
1366        for ty in [
1367            Type::TEXT,
1368            Type::VARCHAR,
1369            Type::BPCHAR,
1370            Type::NAME,
1371            Type::JSON,
1372            Type::BYTEA,
1373            Type::UNKNOWN,
1374        ] {
1375            let mut buffer = bytes::BytesMut::new();
1376            encode_value(&DecodedValue::Str("hello".into()), &ty, &mut buffer)
1377                .unwrap_or_else(|e| panic!("{} must take a string: {e}", ty.name()));
1378            assert_eq!(&buffer[..], b"hello", "{}", ty.name());
1379        }
1380    }
1381
1382    #[test]
1383    fn a_string_still_converts_for_uuid_jsonb_and_numeric() {
1384        let mut buffer = bytes::BytesMut::new();
1385        encode_value(
1386            &DecodedValue::Str("00000000-0000-0000-0000-000000000001".into()),
1387            &Type::UUID,
1388            &mut buffer,
1389        )
1390        .expect("uuid takes a string");
1391        assert_eq!(buffer.len(), 16, "a uuid is converted, not copied");
1392
1393        let mut buffer = bytes::BytesMut::new();
1394        encode_value(&DecodedValue::Str(r#"{"a":1}"#.into()), &Type::JSONB, &mut buffer).expect("jsonb takes a string");
1395        assert_eq!(buffer[0], 1, "jsonb needs its version byte");
1396
1397        let mut buffer = bytes::BytesMut::new();
1398        encode_value(&DecodedValue::Str("12.50".into()), &Type::NUMERIC, &mut buffer).expect("numeric takes a string");
1399        assert_eq!(numeric::decode(&buffer).unwrap(), "12.50");
1400    }
1401
1402    #[test]
1403    fn decodes_jsonb_object() {
1404        let mut data = vec![1u8]; // version prefix
1405        data.extend_from_slice(br#"{"a":1,"b":"two","c":[1,2,3],"d":null}"#);
1406        let decoded = decode_value(OID_JSONB, &data, &no_ext()).unwrap();
1407        assert_eq!(
1408            decoded,
1409            DecodedValue::Object(vec![
1410                ("a".into(), DecodedValue::I64(1)),
1411                ("b".into(), DecodedValue::Str("two".into())),
1412                (
1413                    "c".into(),
1414                    DecodedValue::Array(vec![DecodedValue::I64(1), DecodedValue::I64(2), DecodedValue::I64(3)])
1415                ),
1416                ("d".into(), DecodedValue::Null),
1417            ])
1418        );
1419    }
1420
1421    #[test]
1422    fn decodes_jsonb_scalar_and_array() {
1423        let mut data = vec![1u8];
1424        data.extend_from_slice(b"42");
1425        assert_eq!(
1426            decode_value(OID_JSONB, &data, &no_ext()).unwrap(),
1427            DecodedValue::I64(42)
1428        );
1429
1430        // A number that is not an integer keeps its own digits: jsonb has
1431        // one number type, and only the shape that reads it knows whether
1432        // it was written as a float or a decimal.
1433        let mut data2 = vec![1u8];
1434        data2.extend_from_slice(b"[1.5, 2.5]");
1435        assert_eq!(
1436            decode_value(OID_JSONB, &data2, &no_ext()).unwrap(),
1437            DecodedValue::Array(vec![
1438                DecodedValue::JsonNumber("1.5".to_string()),
1439                DecodedValue::JsonNumber("2.5".to_string()),
1440            ])
1441        );
1442    }
1443
1444    #[test]
1445    fn decodes_a_jsonb_number_as_the_digits_it_was_written_with() {
1446        // The two a float64 would have cost: the scale `12.3400` carries,
1447        // and a value wider than a float can hold.
1448        for digits in [
1449            "12.3400",
1450            "0.00000039999999999999998189924473035450347424557548947632312774658203125",
1451            "1e3",
1452        ] {
1453            let mut data = vec![1u8];
1454            data.extend_from_slice(format!(r#"{{"n": {digits}}}"#).as_bytes());
1455            assert_eq!(
1456                decode_value(OID_JSONB, &data, &no_ext()).unwrap(),
1457                DecodedValue::Object(vec![("n".to_string(), DecodedValue::JsonNumber(digits.to_string()))])
1458            );
1459        }
1460    }
1461
1462    #[test]
1463    fn a_jsonb_number_that_kept_its_digits_goes_back_out_as_a_number() {
1464        // Binding a decoded value back must not quote it or round it.
1465        let value = DecodedValue::Object(vec![("n".to_string(), DecodedValue::JsonNumber("12.3400".to_string()))]);
1466        let mut out = bytes::BytesMut::new();
1467        encode_value(&value, &postgres_types::Type::JSONB, &mut out).unwrap();
1468        assert_eq!(out.as_ref(), [&[1u8][..], br#"{"n":12.3400}"#].concat());
1469    }
1470
1471    #[test]
1472    fn decodes_jsonb_object_keys_in_the_order_it_always_has() {
1473        // `serde_json::Value::Object` is a `BTreeMap` with its own
1474        // `preserve_order` feature off, which is the order every reader of
1475        // these values has seen so far.
1476        let mut data = vec![1u8];
1477        data.extend_from_slice(br#"{"b": 1, "aa": 2}"#);
1478        let DecodedValue::Object(fields) = decode_value(OID_JSONB, &data, &no_ext()).unwrap() else {
1479            panic!("expected an object")
1480        };
1481        assert_eq!(
1482            fields.iter().map(|(k, _)| k.as_str()).collect::<Vec<_>>(),
1483            vec!["aa", "b"]
1484        );
1485    }
1486
1487    /// Builds a `record`-field's binary payload by hand: `i32 nfields`,
1488    /// then per field `u32 type_oid` + `i32 field_len` (`-1` = NULL) +
1489    /// bytes — mirroring exactly what `decode_record` reads.
1490    fn encode_record(fields: &[(u32, Option<&[u8]>)]) -> Vec<u8> {
1491        let mut buf = Vec::new();
1492        buf.extend_from_slice(&(fields.len() as i32).to_be_bytes());
1493        for (oid, data) in fields {
1494            buf.extend_from_slice(&oid.to_be_bytes());
1495            match data {
1496                None => buf.extend_from_slice(&(-1i32).to_be_bytes()),
1497                Some(bytes) => {
1498                    buf.extend_from_slice(&(bytes.len() as i32).to_be_bytes());
1499                    buf.extend_from_slice(bytes);
1500                }
1501            }
1502        }
1503        buf
1504    }
1505
1506    #[test]
1507    fn decodes_flat_record() {
1508        let data = encode_record(&[
1509            (OID_INT8, Some(&42i64.to_be_bytes())),
1510            (OID_TEXT, Some(b"alice")),
1511            (OID_BOOL, None),
1512        ]);
1513        let decoded = decode_value(OID_RECORD, &data, &no_ext()).unwrap();
1514        assert_eq!(
1515            decoded,
1516            DecodedValue::Composite(vec![
1517                DecodedValue::I64(42),
1518                DecodedValue::Str("alice".into()),
1519                DecodedValue::Null
1520            ])
1521        );
1522    }
1523
1524    #[test]
1525    fn decodes_nested_record() {
1526        let inner = encode_record(&[(OID_INT8, Some(&1i64.to_be_bytes()))]);
1527        let outer = encode_record(&[(OID_RECORD, Some(&inner)), (OID_TEXT, Some(b"outer"))]);
1528        let decoded = decode_value(OID_RECORD, &outer, &no_ext()).unwrap();
1529        assert_eq!(
1530            decoded,
1531            DecodedValue::Composite(vec![
1532                DecodedValue::Composite(vec![DecodedValue::I64(1)]),
1533                DecodedValue::Str("outer".into()),
1534            ])
1535        );
1536    }
1537
1538    /// Builds a Postgres array's binary payload by hand: `i32 ndim`, `i32
1539    /// has_null`, `u32 element_oid`, `(i32 dim, i32 lbound)`, then per
1540    /// element `i32 len` (`-1` = NULL) + bytes.
1541    fn encode_array(element_oid: u32, elements: &[Option<&[u8]>]) -> Vec<u8> {
1542        if elements.is_empty() {
1543            let mut buf = Vec::new();
1544            buf.extend_from_slice(&0i32.to_be_bytes());
1545            buf.extend_from_slice(&0i32.to_be_bytes());
1546            buf.extend_from_slice(&element_oid.to_be_bytes());
1547            return buf;
1548        }
1549        let mut buf = Vec::new();
1550        buf.extend_from_slice(&1i32.to_be_bytes());
1551        buf.extend_from_slice(&0i32.to_be_bytes());
1552        buf.extend_from_slice(&element_oid.to_be_bytes());
1553        buf.extend_from_slice(&(elements.len() as i32).to_be_bytes());
1554        buf.extend_from_slice(&1i32.to_be_bytes());
1555        for data in elements {
1556            match data {
1557                None => buf.extend_from_slice(&(-1i32).to_be_bytes()),
1558                Some(bytes) => {
1559                    buf.extend_from_slice(&(bytes.len() as i32).to_be_bytes());
1560                    buf.extend_from_slice(bytes);
1561                }
1562            }
1563        }
1564        buf
1565    }
1566
1567    #[test]
1568    fn decodes_array_of_scalars() {
1569        let data = encode_array(OID_TEXT, &[Some(b"a"), Some(b"b"), None]);
1570        let decoded = decode_value(OID_TEXT_ARRAY, &data, &no_ext()).unwrap();
1571        assert_eq!(
1572            decoded,
1573            DecodedValue::Array(vec![
1574                DecodedValue::Str("a".into()),
1575                DecodedValue::Str("b".into()),
1576                DecodedValue::Null
1577            ])
1578        );
1579    }
1580
1581    #[test]
1582    fn decodes_empty_array() {
1583        let data = encode_array(OID_TEXT, &[]);
1584        assert_eq!(
1585            decode_value(OID_TEXT_ARRAY, &data, &no_ext()).unwrap(),
1586            DecodedValue::Array(vec![])
1587        );
1588    }
1589
1590    #[test]
1591    fn decodes_array_of_records() {
1592        let rec1 = encode_record(&[(OID_INT8, Some(&1i64.to_be_bytes()))]);
1593        let rec2 = encode_record(&[(OID_INT8, Some(&2i64.to_be_bytes()))]);
1594        let data = encode_array(OID_RECORD, &[Some(&rec1), Some(&rec2)]);
1595        let decoded = decode_value(OID_RECORD_ARRAY, &data, &no_ext()).unwrap();
1596        assert_eq!(
1597            decoded,
1598            DecodedValue::Array(vec![
1599                DecodedValue::Composite(vec![DecodedValue::I64(1)]),
1600                DecodedValue::Composite(vec![DecodedValue::I64(2)]),
1601            ])
1602        );
1603    }
1604
1605    #[test]
1606    fn decodes_vector_when_extension_oid_known() {
1607        let mut data = 2u16.to_be_bytes().to_vec(); // ndim = 2
1608        data.extend_from_slice(&0u16.to_be_bytes()); // reserved
1609        data.extend_from_slice(&1.5f32.to_be_bytes());
1610        data.extend_from_slice(&2.5f32.to_be_bytes());
1611
1612        let ext = ExtensionOids {
1613            vector: Some(50_000),
1614            ..Default::default()
1615        };
1616        let decoded = decode_value(50_000, &data, &ext).unwrap();
1617        assert_eq!(
1618            decoded,
1619            DecodedValue::Array(vec![DecodedValue::F64(1.5), DecodedValue::F64(2.5)])
1620        );
1621    }
1622
1623    #[test]
1624    fn unknown_oid_without_vector_extension_is_an_error_not_a_guess() {
1625        // Same OID as the vector test above, but with nothing discovered —
1626        // it must neither be read as vector binary data nor blindly
1627        // stringified, since which of those is right is exactly what
1628        // discovery exists to establish.
1629        let err = decode_value(50_000, "some-domain-value".as_bytes(), &no_ext()).unwrap_err();
1630        assert!(matches!(err, Error::UnknownTypeOid { oid: 50_000 }), "got {err:?}");
1631    }
1632
1633    #[test]
1634    fn encodes_a_str_value_as_uuid_binary_when_the_target_type_is_uuid() {
1635        // Regression test: a JSON API request body carries a UUID query
1636        // parameter as plain text (there's no JSON "uuid" type), so it
1637        // arrives as `DecodedValue::Str` — binding it directly against a
1638        // `uuid`-typed parameter must produce the 16-byte binary form, not
1639        // the raw 36-character text bytes (which Postgres rejects with
1640        // "incorrect binary data format").
1641        let value = DecodedValue::Str("11111111-2222-3333-4444-555555555555".to_string());
1642        let mut out = bytes::BytesMut::new();
1643        encode_value(&value, &postgres_types::Type::UUID, &mut out).unwrap();
1644        assert_eq!(
1645            out.as_ref(),
1646            &[
1647                0x11, 0x11, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44, 0x44, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55
1648            ]
1649        );
1650    }
1651
1652    #[test]
1653    fn a_str_value_still_encodes_as_plain_text_for_a_text_target() {
1654        let value = DecodedValue::Str("11111111-2222-3333-4444-555555555555".to_string());
1655        let mut out = bytes::BytesMut::new();
1656        encode_value(&value, &postgres_types::Type::TEXT, &mut out).unwrap();
1657        assert_eq!(out.as_ref(), "11111111-2222-3333-4444-555555555555".as_bytes());
1658    }
1659
1660    #[test]
1661    fn rejects_a_malformed_uuid_string_instead_of_sending_garbage_bytes() {
1662        let value = DecodedValue::Str("not-a-uuid".to_string());
1663        let mut out = bytes::BytesMut::new();
1664        assert!(encode_value(&value, &postgres_types::Type::UUID, &mut out).is_err());
1665    }
1666
1667    fn vector_type() -> Type {
1668        // `vector` is a pgvector extension type, not a `postgres_types`
1669        // builtin — construct it the way `Statement::params()` would
1670        // report it back (any OID works here; encoding only inspects the
1671        // name via `ty.name()`, matching `$n::vector`'s cast-reported type).
1672        Type::new(
1673            "vector".to_string(),
1674            50_000,
1675            postgres_types::Kind::Simple,
1676            "public".to_string(),
1677        )
1678    }
1679
1680    #[test]
1681    fn encodes_an_array_value_as_pgvector_binary_when_the_target_type_is_vector() {
1682        // Regression test: `$n::vector` reports the parameter's type as
1683        // the scalar `vector` type itself (unlike `$n::float8[]::vector`,
1684        // where the *inner* cast makes Postgres report `float8[]`) — an
1685        // `Array` value bound against it must produce pgvector's own
1686        // binary format (`u16 ndim`, `u16 reserved`, then big-endian
1687        // `f32`s), not the generic Postgres array wire format.
1688        let value = DecodedValue::Array(vec![
1689            DecodedValue::F64(1.5),
1690            DecodedValue::F64(-2.25),
1691            DecodedValue::F64(0.0),
1692        ]);
1693        let mut out = bytes::BytesMut::new();
1694        encode_value(&value, &vector_type(), &mut out).unwrap();
1695        let mut expected = vec![0u8, 3, 0, 0];
1696        expected.extend_from_slice(&1.5f32.to_be_bytes());
1697        expected.extend_from_slice(&(-2.25f32).to_be_bytes());
1698        expected.extend_from_slice(&0.0f32.to_be_bytes());
1699        assert_eq!(out.as_ref(), expected.as_slice());
1700    }
1701
1702    #[test]
1703    fn a_vector_encoded_value_round_trips_through_decode_vector() {
1704        let value = DecodedValue::Array(vec![
1705            DecodedValue::F64(1.0),
1706            DecodedValue::F64(2.0),
1707            DecodedValue::F64(3.0),
1708        ]);
1709        let mut out = bytes::BytesMut::new();
1710        encode_value(&value, &vector_type(), &mut out).unwrap();
1711        let decoded = decode_vector(out.as_ref()).unwrap();
1712        assert_eq!(
1713            decoded,
1714            vec![DecodedValue::F64(1.0), DecodedValue::F64(2.0), DecodedValue::F64(3.0)]
1715        );
1716    }
1717
1718    #[test]
1719    fn an_array_value_still_encodes_as_a_plain_postgres_array_for_a_non_vector_target() {
1720        let value = DecodedValue::Array(vec![DecodedValue::F64(1.0), DecodedValue::F64(2.0)]);
1721        let mut out = bytes::BytesMut::new();
1722        encode_value(&value, &postgres_types::Type::FLOAT8_ARRAY, &mut out).unwrap();
1723        // Generic array format starts with ndim=1 (i32), not pgvector's
1724        // ndim=2 (u16) — first four bytes distinguish the two encodings.
1725        assert_eq!(&out.as_ref()[0..4], &1i32.to_be_bytes());
1726    }
1727
1728    #[test]
1729    fn encodes_a_str_value_as_jsonb_binary_when_the_target_type_is_jsonb() {
1730        // Regression test for the same class of bug as the UUID case above:
1731        // a caller with already-serialized JSON text (e.g.
1732        // `schema_to_db_state_json`'s output) arrives as `DecodedValue::Str`,
1733        // not `::Object` — binding it against a `jsonb` parameter must add
1734        // the binary version-byte prefix, not send raw unframed text.
1735        let value = DecodedValue::Str(r#"{"a":1}"#.to_string());
1736        let mut out = bytes::BytesMut::new();
1737        encode_value(&value, &postgres_types::Type::JSONB, &mut out).unwrap();
1738        assert_eq!(out.as_ref(), [&[1u8][..], br#"{"a":1}"#].concat());
1739        // And decodes back correctly through the normal jsonb decode path.
1740        assert_eq!(
1741            decode_value(OID_JSONB, &out, &no_ext()).unwrap(),
1742            DecodedValue::Object(vec![("a".into(), DecodedValue::I64(1))])
1743        );
1744    }
1745
1746    #[test]
1747    fn encodes_a_composite_as_a_jsonb_array_but_refuses_it_elsewhere() {
1748        // An all-unnamed `tuple<str, bool>` parameter is a jsonb array, and
1749        // JSON needs no per-field Postgres type — unlike a real record,
1750        // which stays refused.
1751        let value = DecodedValue::Composite(vec![DecodedValue::Str("left".into()), DecodedValue::Bool(true)]);
1752        let mut out = bytes::BytesMut::new();
1753        encode_value(&value, &postgres_types::Type::JSONB, &mut out).unwrap();
1754        assert_eq!(out.as_ref(), [&[1u8][..], br#"["left",true]"#].concat());
1755
1756        let mut out = bytes::BytesMut::new();
1757        let refused = encode_value(&value, &postgres_types::Type::RECORD, &mut out);
1758        assert!(
1759            refused.is_err_and(|e| e.to_string().contains("cannot bind a composite value")),
1760            "a record parameter has no per-field type to encode against"
1761        );
1762    }
1763}