pylon_client/value.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//! The crate's generic result value — Rust has no equivalent of the
21//! per-type dataclasses `pylon-py` hydrates results into, so a query result
22//! decodes into this instead: a generic, dynamically-typed value rather
23//! than a per-type generated struct.
24
25use std::ops::Index;
26
27/// A decoded query result value. One variant per `ShapeNode`/`DecodedValue`
28/// kind `decode.rs` knows how to produce — see that module for the walk
29/// that builds these.
30#[derive(Debug, Clone, PartialEq)]
31pub enum Value {
32 Null,
33 Bool(bool),
34 Int64(i64),
35 Float64(f64),
36 Str(String),
37 Bytes(Vec<u8>),
38 Uuid(uuid::Uuid),
39 /// Arbitrary-precision decimal, in its canonical string form — matches
40 /// `pylon_value::DecodedValue::Decimal`'s own representation, since
41 /// there's no single obviously-correct native Rust decimal type to
42 /// commit this generic client to.
43 Decimal(String),
44 /// A PostgreSQL `interval` (`std::duration` / `cal::relative_duration`)
45 /// — kept as its three raw wire components, same reasoning as
46 /// `DecodedValue::Interval`.
47 Duration {
48 months: i32,
49 days: i32,
50 microseconds: i64,
51 },
52 /// Whole days since the PG epoch (2000-01-01). Backs `cal::local_date`.
53 Date(i32),
54 /// Microseconds since midnight. Backs `cal::local_time`.
55 Time(i64),
56 /// Microseconds since the PG epoch, no timezone. Backs `cal::local_datetime`.
57 Timestamp(i64),
58 /// Microseconds since the PG epoch, UTC. Backs `std::datetime`.
59 Timestamptz(i64),
60 Range(Box<Range>),
61 /// A genuine Postgres array.
62 Array(Vec<Value>),
63 /// An anonymous positional tuple (`tuple<...>` with no member names).
64 Tuple(Vec<Value>),
65 /// A schema object or a free/named-tuple object — see `Object`.
66 Object(Object),
67 /// An enum value, hydrated to its qualified type name + variant label
68 /// rather than a generated Rust enum (there's no per-schema-type codegen
69 /// on this client, matching the generic-`Object` design as a whole).
70 Enum {
71 type_name: String,
72 value: String,
73 },
74 /// Result of a `group` statement.
75 Group(Box<Group>),
76 /// Result of a `vector::search` statement.
77 VectorSearch {
78 object: Box<Value>,
79 distance: f64,
80 },
81 /// Result of an `fts::search` statement.
82 FtsSearch {
83 object: Box<Value>,
84 score: f64,
85 },
86}
87
88/// A schema object, a free object literal, or a named tuple — all three
89/// decode to the same by-name field-access shape. `type_name` is `Some`
90/// only for a real schema type (including the concrete type of a
91/// polymorphic interface query result) or a registered named tuple;
92/// `None` for a free object (`select { a := 1 }`) or an unregistered
93/// structural named tuple.
94#[derive(Debug, Clone, PartialEq, Default)]
95pub struct Object {
96 pub(crate) type_name: Option<String>,
97 /// Field order matches the order the query's shape declared them in.
98 pub(crate) fields: Vec<(String, Value)>,
99 /// True when `fields[0]` is the `id` the compiler added to a shape that
100 /// did not select one — see `pylon_core::query::ShapeNode::Object`.
101 pub(crate) implicit_id: bool,
102}
103
104impl Object {
105 pub fn type_name(&self) -> Option<&str> {
106 self.type_name.as_deref()
107 }
108
109 /// Every field, the implicit `id` included — what a caller decoding into
110 /// their own type reads, and what makes `o.id` work on a shape that only
111 /// named other pointers.
112 pub fn fields(&self) -> impl Iterator<Item = (&str, &Value)> {
113 self.fields.iter().map(|(n, v)| (n.as_str(), v))
114 }
115
116 /// The fields JSON output carries — the same, less an `id` nobody asked
117 /// for. Gel's JSON output leaves its own implicit id out too, so a query
118 /// ported from it renders the same document here.
119 pub fn json_fields(&self) -> impl Iterator<Item = (&str, &Value)> {
120 let skip = usize::from(self.implicit_id);
121 self.fields.iter().skip(skip).map(|(n, v)| (n.as_str(), v))
122 }
123
124 pub fn get(&self, name: &str) -> Option<&Value> {
125 self.fields.iter().find(|(n, _)| n == name).map(|(_, v)| v)
126 }
127
128 pub fn len(&self) -> usize {
129 self.fields.len()
130 }
131
132 pub fn is_empty(&self) -> bool {
133 self.fields.is_empty()
134 }
135}
136
137/// Panics if `name` isn't a field on this object.
138impl Index<&str> for Object {
139 type Output = Value;
140
141 fn index(&self, name: &str) -> &Value {
142 self.get(name).unwrap_or_else(|| panic!("Object has no field {name:?}"))
143 }
144}
145
146/// A PostgreSQL range value. `lower`/`upper` are `None` for an unbounded
147/// side; `empty == true` means the whole range is empty (`lower`/`upper`
148/// are meaningless then, not "both unbounded").
149#[derive(Debug, Clone, PartialEq)]
150pub struct Range {
151 pub lower: Option<Value>,
152 pub upper: Option<Value>,
153 pub inc_lower: bool,
154 pub inc_upper: bool,
155 pub empty: bool,
156}
157
158/// Result of a `group` statement: one grouping key, the grouping label
159/// list, and the elements sharing that key.
160#[derive(Debug, Clone, PartialEq)]
161pub struct Group {
162 pub key: Object,
163 pub grouping: Vec<String>,
164 pub elements: Vec<Value>,
165}