Skip to main content

pylon_client/
decode.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//! Walks a compiled query's `ShapeNode` alongside its decoded `DecodedValue`
21//! result to build a generic [`Value`](crate::value::Value) — the Rust
22//! port of `pylon/query.py`'s `_decode`/`_decode_json_tuple`/
23//! `_decode_json_member` (there's no per-type dataclass registry to hydrate
24//! against here, so every object-shaped result becomes a generic
25//! [`Object`](crate::value::Object) instead).
26
27use pylon_core::query::{JsonMember, JsonMemberKind, ShapeNode};
28use pylon_value::DecodedValue;
29
30use crate::value::{Group, Object, Range, Value};
31
32/// Decode `value` (the query's own `result` column, already wire-decoded
33/// into a `DecodedValue`) using `shape` (the compiled query's own output
34/// shape) into the crate's generic [`Value`].
35pub fn decode(shape: &ShapeNode, value: &DecodedValue) -> Value {
36    match shape {
37        // The row is the root object's own tuple.
38        ShapeNode::Object { position: 0, .. } => decode_inner(shape, value, Some(0)),
39        // And a root tuple *is* the row, with nothing to index out of — the
40        // same distinction `decode_row`/`decode_at_root` draw in `pylon-py`.
41        ShapeNode::Tuple { position: 0, .. } => decode_inner(shape, value, Some(0)),
42        _ => decode_inner(shape, value, None),
43    }
44}
45
46/// `position_override` is `Some(0)` when `value` has already been extracted
47/// from its enclosing composite (an array/group element, or a vector/FTS
48/// search's object sub-tuple) — mirrors `pylon/query.py`'s `{**node,
49/// "position": 0}` overrides in `_decode`'s `"array"`/`"group"`/
50/// `"vector_search"`/`"fts_search"` branches.
51fn decode_inner(shape: &ShapeNode, value: &DecodedValue, position_override: Option<usize>) -> Value {
52    match shape {
53        ShapeNode::Scalar { position, .. } => {
54            cached_to_value(&composite_at(value, position_override.unwrap_or(*position)))
55        }
56        ShapeNode::RawScalar | ShapeNode::JsonScalar => cached_to_value(value),
57        ShapeNode::Object {
58            type_name,
59            position,
60            pointers,
61            has_implicit_id,
62            ..
63        } => {
64            // An object at position 0 of the enclosing tuple is a free
65            // object's first field, not the tuple itself.
66            let obj_tuple = match position_override {
67                Some(_) => value.clone(),
68                None => composite_at(value, *position),
69            };
70            decode_object(&obj_tuple, type_name.as_deref(), pointers, *has_implicit_id)
71        }
72        ShapeNode::Array { position, element, .. } => {
73            decode_array(value, position_override.unwrap_or(*position), element)
74        }
75        ShapeNode::NamedTuple {
76            position,
77            type_name,
78            members,
79            ..
80        } => decode_named_tuple(
81            value,
82            position_override.unwrap_or(*position),
83            type_name.as_deref(),
84            members.as_deref(),
85        ),
86        ShapeNode::Enum {
87            position, enum_type, ..
88        } => decode_enum(value, position_override.unwrap_or(*position), enum_type),
89        // A nested tuple is a composite inside the row, indexed out before
90        // its elements are read at their own positions within it. At the
91        // root the row *is* the tuple, which `position_override` says.
92        ShapeNode::Tuple {
93            position,
94            elements,
95            names,
96            type_name,
97            ..
98        } => {
99            let row = match position_override {
100                Some(_) => value.clone(),
101                None => composite_at(value, *position),
102            };
103            decode_composite_tuple(&row, elements, names.as_deref(), type_name.as_deref())
104        }
105        ShapeNode::Group {
106            key_nodes,
107            grouping_position,
108            elements_position,
109            element,
110        } => decode_group(value, key_nodes, *grouping_position, *elements_position, element),
111        ShapeNode::VectorSearch {
112            object_position,
113            distance_position,
114            object_node,
115        } => decode_vector_search(value, *object_position, *distance_position, object_node),
116        ShapeNode::FtsSearch {
117            object_position,
118            rank_position,
119            object_node,
120        } => decode_fts_search(value, *object_position, *rank_position, object_node),
121    }
122}
123
124/// Extracts the field at `pos` from a composite (tuple/`ROW(...)`) value —
125/// the Rust equivalent of Python's `value[pos]` on a record row.
126/// Anything else (a bare scalar reached with `pos == 0`, or a genuinely
127/// out-of-range position) degrades to `Null` rather than panicking — the
128/// shape and the SQL that produced `value` are always built together by
129/// the same compiler pass, so a mismatch here would be an internal
130/// compiler bug, not a case the caller needs to recover from gracefully.
131fn composite_at(value: &DecodedValue, pos: usize) -> DecodedValue {
132    match value {
133        DecodedValue::Composite(fields) => fields.get(pos).cloned().unwrap_or(DecodedValue::Null),
134        _ => DecodedValue::Null,
135    }
136}
137
138/// A pointer's own output name — only `Scalar`/`Enum`/`NamedTuple`/
139/// `Object`/`Array` ever appear inside an `Object`'s own `pointers` list
140/// (mirrors `pylon-py`'s `shape_node_to_py`, where `RawScalar`/`JsonScalar`
141/// carry no name/position at all and so can only ever be a shape's root).
142fn pointer_name(node: &ShapeNode) -> &str {
143    match node {
144        ShapeNode::Scalar { name, .. }
145        | ShapeNode::Enum { name, .. }
146        | ShapeNode::NamedTuple { name, .. }
147        | ShapeNode::Tuple { name, .. }
148        | ShapeNode::Object { name, .. }
149        | ShapeNode::Array { name, .. } => name,
150        other => unreachable!("shape node kind never appears as an object's own pointer: {other:?}"),
151    }
152}
153
154fn pointer_position(node: &ShapeNode) -> usize {
155    match node {
156        ShapeNode::Scalar { position, .. }
157        | ShapeNode::Enum { position, .. }
158        | ShapeNode::NamedTuple { position, .. }
159        | ShapeNode::Object { position, .. }
160        | ShapeNode::Array { position, .. } => *position,
161        other => unreachable!("shape node kind never appears as an object's own pointer: {other:?}"),
162    }
163}
164
165/// `ShapeNode::Enum.enum_type` carries the Postgres-schema-qualified form
166/// (e.g. `public::Gender`) — only the `default` module is ever renamed (to
167/// Postgres `public`), so that's the only translation to undo, mirroring
168/// `pylon/query.py`'s `_pylon_qualify_enum_type`/`_pg_schema_to_pylon_module`.
169fn pg_schema_qualified_to_pylon(qualified: &str) -> String {
170    match qualified.split_once("::") {
171        Some(("public", rest)) => format!("default::{rest}"),
172        _ => qualified.to_string(),
173    }
174}
175
176fn decode_object(
177    obj_tuple: &DecodedValue,
178    type_name: Option<&str>,
179    pointers: &[ShapeNode],
180    implicit_id: bool,
181) -> Value {
182    if matches!(obj_tuple, DecodedValue::Null) {
183        return Value::Null;
184    }
185    // pointers[0] is always the auto-injected __type__ discriminator
186    // (position 0); skip it — an explicit __type__ the user asked for
187    // appears at position > 0 and is included like any other field.
188    let mut fields: Vec<(String, Value)> = Vec::new();
189    for p in pointers
190        .iter()
191        .filter(|p| !(pointer_name(p) == "__type__" && pointer_position(p) == 0))
192    {
193        let value = decode_inner(p, obj_tuple, None);
194        // A name repeats only where splats overlap (`*` beside
195        // `[is Sub].*`, or two intersections), and an intersection the row
196        // is not of reads nothing: the value it does have wins.
197        match fields.iter_mut().find(|(name, _)| name == pointer_name(p)) {
198            Some((_, existing)) if is_nothing(existing) => *existing = value,
199            Some(_) => {}
200            None => fields.push((pointer_name(p).to_string(), value)),
201        }
202    }
203    // Use the actual per-row __type__ value for the reported type name —
204    // for a polymorphic (interface) query this is the real concrete type,
205    // not the interface's own static type_name.
206    let resolved_type_name = type_name.map(|static_name| match composite_at(obj_tuple, 0) {
207        DecodedValue::Str(s) if !s.is_empty() => s,
208        _ => static_name.to_string(),
209    });
210    Value::Object(Object {
211        type_name: resolved_type_name,
212        fields,
213        implicit_id,
214    })
215}
216
217fn is_nothing(value: &Value) -> bool {
218    match value {
219        Value::Null => true,
220        Value::Array(items) => items.is_empty(),
221        _ => false,
222    }
223}
224
225fn decode_array(value: &DecodedValue, position: usize, element: &ShapeNode) -> Value {
226    // A set of objects is never NULL; an unset array-typed property is.
227    let items = match composite_at(value, position) {
228        DecodedValue::Array(items) => items,
229        DecodedValue::Null if !matches!(element, ShapeNode::Object { .. }) => return Value::Null,
230        _ => vec![],
231    };
232    Value::Array(items.iter().map(|item| decode_inner(element, item, Some(0))).collect())
233}
234
235/// A tuple read as the composite row it is — each element at its own
236/// position inside `row`, every member already carrying its own PostgreSQL
237/// type rather than being recovered from a jsonb number.
238///
239/// A positional tuple is a `Value::Tuple`; a named one is a `Value::Object`,
240/// the same shape `decode_json_tuple` gives a named tuple, so a caller reads
241/// one the same way however it was stored.
242fn decode_composite_tuple(
243    row: &DecodedValue,
244    elements: &[ShapeNode],
245    names: Option<&[String]>,
246    type_name: Option<&str>,
247) -> Value {
248    if matches!(row, DecodedValue::Null) {
249        return Value::Null;
250    }
251    let items: Vec<Value> = elements.iter().map(|e| decode_inner(e, row, None)).collect();
252    match names {
253        None => Value::Tuple(items),
254        Some(names) => Value::Object(Object {
255            type_name: type_name.map(str::to_string),
256            fields: names.iter().cloned().zip(items).collect(),
257            implicit_id: false,
258        }),
259    }
260}
261
262fn decode_named_tuple(
263    value: &DecodedValue,
264    position: usize,
265    type_name: Option<&str>,
266    members: Option<&[JsonMember]>,
267) -> Value {
268    // Root-level named tuples arrive as the raw decoded jsonb value
269    // directly (Object for named members, Array for positional/unnamed
270    // ones, or Null); a nested one sits at a positional index inside the
271    // parent composite row.
272    let raw = match value {
273        DecodedValue::Object(_) | DecodedValue::Array(_) | DecodedValue::Null => value.clone(),
274        _ => composite_at(value, position),
275    };
276    decode_json_tuple(&raw, type_name, members)
277}
278
279/// Decodes a jsonb tuple/named-tuple value using its statically-known
280/// member shape — a `Value::Tuple` for positional members, a `Value::Object`
281/// for named members (nominal or structural — this generic client has no
282/// registry to hydrate a nominal type against, so both decode the same
283/// way, `type_name` carried along only as metadata). Falls back to a
284/// generic structural conversion when no member shape was known at compile
285/// time.
286fn decode_json_tuple(value: &DecodedValue, type_name: Option<&str>, members: Option<&[JsonMember]>) -> Value {
287    if matches!(value, DecodedValue::Null) {
288        return Value::Null;
289    }
290    let Some(members) = members else {
291        return cached_to_value(value);
292    };
293    let positional = members.iter().all(|m| m.key.is_none());
294    if positional {
295        let items: &[DecodedValue] = match value {
296            DecodedValue::Array(a) => a.as_slice(),
297            _ => &[],
298        };
299        let elements = members
300            .iter()
301            .enumerate()
302            .map(|(i, m)| decode_json_member(items.get(i).unwrap_or(&DecodedValue::Null), m))
303            .collect();
304        return Value::Tuple(elements);
305    }
306    let obj_fields: &[(String, DecodedValue)] = match value {
307        DecodedValue::Object(o) => o.as_slice(),
308        _ => &[],
309    };
310    let fields = members
311        .iter()
312        .map(|m| {
313            let key = m.key.clone().expect("named branch: every member has a key");
314            let raw = obj_fields
315                .iter()
316                .find(|(k, _)| *k == key)
317                .map(|(_, v)| v)
318                .unwrap_or(&DecodedValue::Null);
319            (key, decode_json_member(raw, m))
320        })
321        .collect();
322    Value::Object(Object {
323        type_name: type_name.map(str::to_string),
324        fields,
325        implicit_id: false,
326    })
327}
328
329fn decode_json_member(value: &DecodedValue, member: &JsonMember) -> Value {
330    match &member.kind {
331        JsonMemberKind::Scalar => cached_to_value(value),
332        // The declaration is the only thing that knows: jsonb has one
333        // number type, and the digits reached here as written precisely so
334        // this could build the decimal they spell.
335        JsonMemberKind::Decimal => match value {
336            DecodedValue::JsonNumber(digits) | DecodedValue::Str(digits) => Value::Decimal(digits.clone()),
337            DecodedValue::I64(integer) => Value::Decimal(integer.to_string()),
338            other => cached_to_value(other),
339        },
340        JsonMemberKind::Enum { enum_type } => match value {
341            DecodedValue::Null => Value::Null,
342            DecodedValue::Str(s) => Value::Enum {
343                type_name: pg_schema_qualified_to_pylon(enum_type),
344                value: s.clone(),
345            },
346            other => cached_to_value(other),
347        },
348        JsonMemberKind::Tuple { type_name, members } => decode_json_tuple(value, type_name.as_deref(), Some(members)),
349    }
350}
351
352fn decode_enum(value: &DecodedValue, position: usize, enum_type: &str) -> Value {
353    // An enum inside an array arrives as the label itself rather than as a
354    // field of a record -- the same distinction `decode_named_tuple` draws.
355    let raw = match value {
356        DecodedValue::Composite(_) => composite_at(value, position),
357        other => other.clone(),
358    };
359    match raw {
360        DecodedValue::Null => Value::Null,
361        DecodedValue::Str(s) => Value::Enum {
362            type_name: pg_schema_qualified_to_pylon(enum_type),
363            value: s,
364        },
365        other => cached_to_value(&other),
366    }
367}
368
369fn decode_group(
370    value: &DecodedValue,
371    key_nodes: &[ShapeNode],
372    grouping_position: usize,
373    elements_position: usize,
374    element: &ShapeNode,
375) -> Value {
376    let key_fields: Vec<(String, Value)> = key_nodes
377        .iter()
378        .map(|kn| (pointer_name(kn).to_string(), decode_inner(kn, value, None)))
379        .collect();
380    let key = Object {
381        type_name: None,
382        fields: key_fields,
383        implicit_id: false,
384    };
385
386    let grouping = match composite_at(value, grouping_position) {
387        DecodedValue::Array(items) => items
388            .into_iter()
389            .filter_map(|v| match v {
390                DecodedValue::Str(s) => Some(s),
391                _ => None,
392            })
393            .collect(),
394        _ => vec![],
395    };
396
397    let elements = match composite_at(value, elements_position) {
398        DecodedValue::Array(items) => items.iter().map(|item| decode_inner(element, item, Some(0))).collect(),
399        _ => vec![],
400    };
401
402    Value::Group(Box::new(Group {
403        key,
404        grouping,
405        elements,
406    }))
407}
408
409fn decode_vector_search(
410    value: &DecodedValue,
411    object_position: usize,
412    distance_position: usize,
413    object_node: &ShapeNode,
414) -> Value {
415    let obj_tuple = composite_at(value, object_position);
416    let distance = as_f64(&composite_at(value, distance_position));
417    let object = decode_inner(object_node, &obj_tuple, Some(0));
418    Value::VectorSearch {
419        object: Box::new(object),
420        distance,
421    }
422}
423
424fn decode_fts_search(
425    value: &DecodedValue,
426    object_position: usize,
427    rank_position: usize,
428    object_node: &ShapeNode,
429) -> Value {
430    let obj_tuple = composite_at(value, object_position);
431    let score = as_f64(&composite_at(value, rank_position));
432    let object = decode_inner(object_node, &obj_tuple, Some(0));
433    Value::FtsSearch {
434        object: Box::new(object),
435        score,
436    }
437}
438
439fn as_f64(value: &DecodedValue) -> f64 {
440    match value {
441        DecodedValue::F64(f) => *f,
442        DecodedValue::I64(i) => *i as f64,
443        _ => 0.0,
444    }
445}
446
447/// Generic structural conversion with no shape metadata to guide it — used
448/// for `RawScalar`/`JsonScalar` leaves (the value's own native decoded type
449/// is already correct) and as the fallback when no member shape was known
450/// at compile time.
451pub(crate) fn cached_to_value(value: &DecodedValue) -> Value {
452    match value {
453        DecodedValue::Null => Value::Null,
454        DecodedValue::Bool(b) => Value::Bool(*b),
455        DecodedValue::I64(i) => Value::Int64(*i),
456        DecodedValue::F64(f) => Value::Float64(*f),
457        DecodedValue::Str(s) => Value::Str(s.clone()),
458        DecodedValue::Bytes(b) => Value::Bytes(b.clone()),
459        DecodedValue::Uuid(bytes) => Value::Uuid(uuid::Uuid::from_bytes(*bytes)),
460        DecodedValue::Decimal(s) => Value::Decimal(s.clone()),
461        // A jsonb number with nothing to say it is a decimal reads as the
462        // float it always has (see `JsonMemberKind::Decimal` for the other
463        // half).
464        DecodedValue::JsonNumber(digits) => Value::Float64(digits.parse().unwrap_or(f64::NAN)),
465        DecodedValue::Interval {
466            months,
467            days,
468            microseconds,
469        } => Value::Duration {
470            months: *months,
471            days: *days,
472            microseconds: *microseconds,
473        },
474        DecodedValue::Date(d) => Value::Date(*d),
475        DecodedValue::Time(t) => Value::Time(*t),
476        DecodedValue::Timestamp(t) => Value::Timestamp(*t),
477        DecodedValue::Timestamptz(t) => Value::Timestamptz(*t),
478        DecodedValue::Array(items) => Value::Array(items.iter().map(cached_to_value).collect()),
479        DecodedValue::Composite(items) => Value::Tuple(items.iter().map(cached_to_value).collect()),
480        DecodedValue::Object(fields) => Value::Object(Object {
481            type_name: None,
482            fields: fields.iter().map(|(k, v)| (k.clone(), cached_to_value(v))).collect(),
483            implicit_id: false,
484        }),
485        DecodedValue::Range {
486            lower,
487            upper,
488            inc_lower,
489            inc_upper,
490            empty,
491        } => Value::Range(Box::new(Range {
492            lower: lower.as_ref().map(|b| cached_to_value(b)),
493            upper: upper.as_ref().map(|b| cached_to_value(b)),
494            inc_lower: *inc_lower,
495            inc_upper: *inc_upper,
496            empty: *empty,
497        })),
498    }
499}
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504    use pylon_core::query::Cardinality;
505
506    fn comp(items: Vec<DecodedValue>) -> DecodedValue {
507        DecodedValue::Composite(items)
508    }
509
510    #[test]
511    fn decodes_a_root_object_with_a_scalar_property() {
512        // (type_disc, name) — the type discriminator always sits at
513        // position 0, matching `build_shape`'s own convention.
514        let value = comp(vec![
515            DecodedValue::Str("default::Person".into()),
516            DecodedValue::Str("Bob".into()),
517        ]);
518        let shape = ShapeNode::Object {
519            name: String::new(),
520            type_name: Some("default::Person".into()),
521            position: 0,
522            cardinality: Cardinality::Required,
523            pointers: vec![
524                ShapeNode::Scalar {
525                    name: "__type__".into(),
526                    position: 0,
527                },
528                ShapeNode::Scalar {
529                    name: "name".into(),
530                    position: 1,
531                },
532            ],
533            has_implicit_id: false,
534        };
535        let decoded = decode(&shape, &value);
536        let Value::Object(obj) = decoded else {
537            panic!("expected Object, got {decoded:?}")
538        };
539        assert_eq!(obj.type_name(), Some("default::Person"));
540        assert_eq!(obj.get("name"), Some(&Value::Str("Bob".into())));
541        // The injected __type__ pointer at position 0 must not appear as a
542        // field of its own.
543        assert_eq!(obj.get("__type__"), None);
544        assert_eq!(obj.len(), 1);
545    }
546
547    /// `select Person { name }` — the compiler puts an `id` in front that
548    /// the query never named. A caller decoding the row must see it (that is
549    /// the whole point: `o.id` has to work), and JSON output must not, which
550    /// carries no implicit id.
551    #[test]
552    fn an_implicit_id_decodes_as_a_field_but_is_left_out_of_json() {
553        let id = uuid::Uuid::from_u128(0x0199c3e1_9702_795c_b787_f93f7a88fd4c);
554        let value = comp(vec![
555            DecodedValue::Str("default::Person".into()),
556            DecodedValue::Uuid(*id.as_bytes()),
557            DecodedValue::Str("Bob".into()),
558        ]);
559        let shape = ShapeNode::Object {
560            name: String::new(),
561            type_name: Some("default::Person".into()),
562            position: 0,
563            cardinality: Cardinality::Required,
564            pointers: vec![
565                ShapeNode::Scalar {
566                    name: "__type__".into(),
567                    position: 0,
568                },
569                ShapeNode::Scalar {
570                    name: "id".into(),
571                    position: 1,
572                },
573                ShapeNode::Scalar {
574                    name: "name".into(),
575                    position: 2,
576                },
577            ],
578            has_implicit_id: true,
579        };
580
581        let decoded = decode(&shape, &value);
582        let Value::Object(obj) = &decoded else {
583            panic!("expected Object, got {decoded:?}")
584        };
585        assert_eq!(obj.get("id"), Some(&Value::Uuid(id)));
586        assert_eq!(obj.len(), 2);
587        assert_eq!(crate::json::to_json(&decoded), r#"{"name": "Bob"}"#);
588    }
589
590    /// The same row for a query that wrote `{ id, name }` itself: nothing was
591    /// injected, so the id is the caller's and JSON keeps it.
592    #[test]
593    fn an_id_the_query_asked_for_is_rendered_as_json() {
594        let id = uuid::Uuid::from_u128(0x0199c3e1_9702_795c_b787_f93f7a88fd4c);
595        let value = comp(vec![
596            DecodedValue::Str("default::Person".into()),
597            DecodedValue::Uuid(*id.as_bytes()),
598            DecodedValue::Str("Bob".into()),
599        ]);
600        let shape = ShapeNode::Object {
601            name: String::new(),
602            type_name: Some("default::Person".into()),
603            position: 0,
604            cardinality: Cardinality::Required,
605            pointers: vec![
606                ShapeNode::Scalar {
607                    name: "__type__".into(),
608                    position: 0,
609                },
610                ShapeNode::Scalar {
611                    name: "id".into(),
612                    position: 1,
613                },
614                ShapeNode::Scalar {
615                    name: "name".into(),
616                    position: 2,
617                },
618            ],
619            has_implicit_id: false,
620        };
621
622        let decoded = decode(&shape, &value);
623        assert_eq!(
624            crate::json::to_json(&decoded),
625            r#"{"id": "0199c3e1-9702-795c-b787-f93f7a88fd4c", "name": "Bob"}"#
626        );
627    }
628
629    #[test]
630    fn a_free_objects_first_field_is_read_at_its_position() {
631        // `select { b := (…).latest_data { address } }` — no discriminator in
632        // a free object, so its first field sits at position 0.
633        let value = comp(vec![comp(vec![
634            DecodedValue::Str("default::Address".into()),
635            DecodedValue::Str("Main St".into()),
636        ])]);
637        let shape = ShapeNode::Object {
638            name: String::new(),
639            type_name: None,
640            position: 0,
641            cardinality: Cardinality::Many,
642            pointers: vec![ShapeNode::Object {
643                name: "b".into(),
644                type_name: Some("default::Address".into()),
645                position: 0,
646                cardinality: Cardinality::Optional,
647                pointers: vec![
648                    ShapeNode::Scalar {
649                        name: "__type__".into(),
650                        position: 0,
651                    },
652                    ShapeNode::Scalar {
653                        name: "address".into(),
654                        position: 1,
655                    },
656                ],
657                has_implicit_id: false,
658            }],
659            has_implicit_id: false,
660        };
661        let Value::Object(root) = decode(&shape, &value) else {
662            panic!()
663        };
664        let Some(Value::Object(b)) = root.get("b") else {
665            panic!("{root:?}")
666        };
667        assert_eq!(b.get("address"), Some(&Value::Str("Main St".into())));
668    }
669
670    #[test]
671    fn a_name_two_splats_share_keeps_the_value_it_has() {
672        // `[is A].*` beside `[is B].*`: the intersection the row is not of
673        // reads nothing, whichever comes last.
674        let value = comp(vec![
675            DecodedValue::Str("default::A".into()),
676            DecodedValue::Str("kept".into()),
677            DecodedValue::Null,
678        ]);
679        let shape = ShapeNode::Object {
680            name: String::new(),
681            type_name: Some("default::A".into()),
682            position: 0,
683            cardinality: Cardinality::Required,
684            pointers: vec![
685                ShapeNode::Scalar {
686                    name: "__type__".into(),
687                    position: 0,
688                },
689                ShapeNode::Scalar {
690                    name: "identifier".into(),
691                    position: 1,
692                },
693                ShapeNode::Scalar {
694                    name: "identifier".into(),
695                    position: 2,
696                },
697            ],
698            has_implicit_id: false,
699        };
700        let Value::Object(obj) = decode(&shape, &value) else {
701            panic!("expected Object")
702        };
703        assert_eq!(obj.get("identifier"), Some(&Value::Str("kept".into())));
704        assert_eq!(obj.len(), 1);
705    }
706
707    #[test]
708    fn polymorphic_object_uses_the_actual_row_type_not_the_static_one() {
709        let value = comp(vec![DecodedValue::Str("default::Individual".into())]);
710        let shape = ShapeNode::Object {
711            name: String::new(),
712            type_name: Some("default::Account".into()),
713            position: 0,
714            cardinality: Cardinality::Required,
715            pointers: vec![ShapeNode::Scalar {
716                name: "__type__".into(),
717                position: 0,
718            }],
719            has_implicit_id: false,
720        };
721        let Value::Object(obj) = decode(&shape, &value) else {
722            panic!("expected Object")
723        };
724        assert_eq!(obj.type_name(), Some("default::Individual"));
725    }
726
727    #[test]
728    fn null_object_decodes_to_null() {
729        let value = comp(vec![DecodedValue::Null, DecodedValue::Null]);
730        let shape = ShapeNode::Object {
731            name: "employer".into(),
732            type_name: Some("default::Company".into()),
733            position: 1,
734            cardinality: Cardinality::Optional,
735            pointers: vec![],
736            has_implicit_id: false,
737        };
738        assert_eq!(decode(&shape, &value), Value::Null);
739    }
740
741    #[test]
742    fn decodes_a_nested_array_of_objects() {
743        let item = comp(vec![
744            DecodedValue::Str("default::Tag".into()),
745            DecodedValue::Str("rust".into()),
746        ]);
747        let value = comp(vec![
748            DecodedValue::Str("default::Post".into()),
749            DecodedValue::Array(vec![item]),
750        ]);
751        let shape = ShapeNode::Array {
752            name: "tags".into(),
753            position: 1,
754            element: Box::new(ShapeNode::Object {
755                name: String::new(),
756                type_name: Some("default::Tag".into()),
757                position: 0,
758                cardinality: Cardinality::Many,
759                pointers: vec![
760                    ShapeNode::Scalar {
761                        name: "__type__".into(),
762                        position: 0,
763                    },
764                    ShapeNode::Scalar {
765                        name: "name".into(),
766                        position: 1,
767                    },
768                ],
769                has_implicit_id: false,
770            }),
771        };
772        let Value::Array(items) = decode(&shape, &value) else {
773            panic!("expected Array")
774        };
775        assert_eq!(items.len(), 1);
776        let Value::Object(tag) = &items[0] else {
777            panic!("expected Object element")
778        };
779        assert_eq!(tag.get("name"), Some(&Value::Str("rust".into())));
780    }
781
782    #[test]
783    fn an_unset_array_property_decodes_to_null() {
784        let value = comp(vec![DecodedValue::Str("default::Person".into()), DecodedValue::Null]);
785        let shape = ShapeNode::Array {
786            name: "roles".into(),
787            position: 1,
788            element: Box::new(ShapeNode::Enum {
789                name: String::new(),
790                position: 0,
791                enum_type: "default::Role".into(),
792            }),
793        };
794        assert_eq!(decode(&shape, &value), Value::Null);
795    }
796
797    #[test]
798    fn enum_translates_public_schema_back_to_default_module() {
799        let value = comp(vec![DecodedValue::Str("Male".into())]);
800        let shape = ShapeNode::Enum {
801            name: "gender".into(),
802            position: 0,
803            enum_type: "public::Gender".into(),
804        };
805        assert_eq!(
806            decode(&shape, &value),
807            Value::Enum {
808                type_name: "default::Gender".into(),
809                value: "Male".into()
810            },
811        );
812    }
813
814    #[test]
815    fn enum_in_a_non_default_module_is_left_unqualified_untranslated() {
816        let value = comp(vec![DecodedValue::Str("Active".into())]);
817        let shape = ShapeNode::Enum {
818            name: "status".into(),
819            position: 0,
820            enum_type: "billing::Status".into(),
821        };
822        assert_eq!(
823            decode(&shape, &value),
824            Value::Enum {
825                type_name: "billing::Status".into(),
826                value: "Active".into()
827            },
828        );
829    }
830
831    #[test]
832    fn raw_scalar_and_json_scalar_pass_the_value_through_directly() {
833        let value = DecodedValue::Array(vec![DecodedValue::I64(1), DecodedValue::I64(2)]);
834        assert_eq!(
835            decode(&ShapeNode::RawScalar, &value),
836            Value::Array(vec![Value::Int64(1), Value::Int64(2)])
837        );
838        assert_eq!(
839            decode(&ShapeNode::JsonScalar, &value),
840            Value::Array(vec![Value::Int64(1), Value::Int64(2)])
841        );
842    }
843
844    #[test]
845    fn decodes_a_positional_structural_tuple() {
846        let value = comp(vec![DecodedValue::I64(1), DecodedValue::I64(2)]);
847        let shape = ShapeNode::Tuple {
848            name: String::new(),
849            position: 0,
850            elements: vec![
851                ShapeNode::Scalar {
852                    name: String::new(),
853                    position: 0,
854                },
855                ShapeNode::Scalar {
856                    name: String::new(),
857                    position: 1,
858                },
859            ],
860            names: None,
861            type_name: None,
862        };
863        assert_eq!(
864            decode(&shape, &value),
865            Value::Tuple(vec![Value::Int64(1), Value::Int64(2)])
866        );
867    }
868
869    #[test]
870    fn decodes_a_registered_named_tuple_from_jsonb() {
871        let raw = DecodedValue::Object(vec![
872            ("x".into(), DecodedValue::F64(1.0)),
873            ("y".into(), DecodedValue::F64(2.0)),
874        ]);
875        let value = comp(vec![DecodedValue::Str("default::Person".into()), raw]);
876        let shape = ShapeNode::NamedTuple {
877            name: "location".into(),
878            position: 1,
879            type_name: Some("default::Point".into()),
880            members: Some(vec![
881                JsonMember {
882                    key: Some("x".into()),
883                    kind: JsonMemberKind::Scalar,
884                },
885                JsonMember {
886                    key: Some("y".into()),
887                    kind: JsonMemberKind::Scalar,
888                },
889            ]),
890            is_free_object: false,
891        };
892        let Value::Object(point) = decode(&shape, &value) else {
893            panic!("expected Object")
894        };
895        assert_eq!(point.type_name(), Some("default::Point"));
896        assert_eq!(point.get("x"), Some(&Value::Float64(1.0)));
897        assert_eq!(point.get("y"), Some(&Value::Float64(2.0)));
898    }
899
900    #[test]
901    fn decodes_an_unregistered_structural_named_tuple_member_enum() {
902        let raw = DecodedValue::Object(vec![("gender".into(), DecodedValue::Str("Female".into()))]);
903        let shape = ShapeNode::NamedTuple {
904            name: String::new(),
905            position: 0,
906            type_name: None,
907            members: Some(vec![JsonMember {
908                key: Some("gender".into()),
909                kind: JsonMemberKind::Enum {
910                    enum_type: "public::Gender".into(),
911                },
912            }]),
913            is_free_object: false,
914        };
915        let Value::Object(obj) = decode(&shape, &raw) else {
916            panic!("expected Object")
917        };
918        assert_eq!(obj.type_name(), None);
919        assert_eq!(
920            obj.get("gender"),
921            Some(&Value::Enum {
922                type_name: "default::Gender".into(),
923                value: "Female".into()
924            }),
925        );
926    }
927
928    #[test]
929    fn decodes_group_result() {
930        let value = comp(vec![
931            DecodedValue::Str("Male".into()),
932            DecodedValue::Array(vec![DecodedValue::Str("gender".into())]),
933            DecodedValue::Array(vec![comp(vec![
934                DecodedValue::Str("default::Person".into()),
935                DecodedValue::Str("Bob".into()),
936            ])]),
937        ]);
938        let shape = ShapeNode::Group {
939            key_nodes: vec![ShapeNode::Scalar {
940                name: "gender".into(),
941                position: 0,
942            }],
943            grouping_position: 1,
944            elements_position: 2,
945            element: Box::new(ShapeNode::Object {
946                name: String::new(),
947                type_name: Some("default::Person".into()),
948                position: 0,
949                cardinality: Cardinality::Many,
950                pointers: vec![
951                    ShapeNode::Scalar {
952                        name: "__type__".into(),
953                        position: 0,
954                    },
955                    ShapeNode::Scalar {
956                        name: "name".into(),
957                        position: 1,
958                    },
959                ],
960                has_implicit_id: false,
961            }),
962        };
963        let Value::Group(group) = decode(&shape, &value) else {
964            panic!("expected Group")
965        };
966        assert_eq!(group.key.get("gender"), Some(&Value::Str("Male".into())));
967        assert_eq!(group.grouping, vec!["gender".to_string()]);
968        assert_eq!(group.elements.len(), 1);
969        let Value::Object(person) = &group.elements[0] else {
970            panic!("expected Object element")
971        };
972        assert_eq!(person.get("name"), Some(&Value::Str("Bob".into())));
973    }
974
975    #[test]
976    fn decodes_vector_search_result() {
977        let obj = comp(vec![
978            DecodedValue::Str("default::Product".into()),
979            DecodedValue::Str("Widget".into()),
980        ]);
981        let value = comp(vec![DecodedValue::Null, obj, DecodedValue::F64(0.25)]);
982        let shape = ShapeNode::VectorSearch {
983            object_position: 1,
984            distance_position: 2,
985            object_node: Box::new(ShapeNode::Object {
986                name: "object".into(),
987                type_name: Some("default::Product".into()),
988                position: 1,
989                cardinality: Cardinality::Many,
990                pointers: vec![
991                    ShapeNode::Scalar {
992                        name: "__type__".into(),
993                        position: 0,
994                    },
995                    ShapeNode::Scalar {
996                        name: "name".into(),
997                        position: 1,
998                    },
999                ],
1000                has_implicit_id: false,
1001            }),
1002        };
1003        let Value::VectorSearch { object, distance } = decode(&shape, &value) else {
1004            panic!("expected VectorSearch")
1005        };
1006        assert_eq!(distance, 0.25);
1007        let Value::Object(product) = *object else {
1008            panic!("expected Object")
1009        };
1010        assert_eq!(product.get("name"), Some(&Value::Str("Widget".into())));
1011    }
1012
1013    #[test]
1014    fn cached_to_value_converts_a_range() {
1015        let value = DecodedValue::Range {
1016            lower: Some(Box::new(DecodedValue::I64(1))),
1017            upper: Some(Box::new(DecodedValue::I64(10))),
1018            inc_lower: true,
1019            inc_upper: false,
1020            empty: false,
1021        };
1022        let Value::Range(range) = cached_to_value(&value) else {
1023            panic!("expected Range")
1024        };
1025        assert_eq!(range.lower, Some(Value::Int64(1)));
1026        assert_eq!(range.upper, Some(Value::Int64(10)));
1027        assert!(range.inc_lower && !range.inc_upper && !range.empty);
1028    }
1029}