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. A rendered document holds exactly what the shape named.
118 pub fn json_fields(&self) -> impl Iterator<Item = (&str, &Value)> {
119 let skip = usize::from(self.implicit_id);
120 self.fields.iter().skip(skip).map(|(n, v)| (n.as_str(), v))
121 }
122
123 pub fn get(&self, name: &str) -> Option<&Value> {
124 self.fields.iter().find(|(n, _)| n == name).map(|(_, v)| v)
125 }
126
127 pub fn len(&self) -> usize {
128 self.fields.len()
129 }
130
131 pub fn is_empty(&self) -> bool {
132 self.fields.is_empty()
133 }
134}
135
136/// Panics if `name` isn't a field on this object.
137impl Index<&str> for Object {
138 type Output = Value;
139
140 fn index(&self, name: &str) -> &Value {
141 self.get(name).unwrap_or_else(|| panic!("Object has no field {name:?}"))
142 }
143}
144
145/// A PostgreSQL range value. `lower`/`upper` are `None` for an unbounded
146/// side; `empty == true` means the whole range is empty (`lower`/`upper`
147/// are meaningless then, not "both unbounded").
148#[derive(Debug, Clone, PartialEq)]
149pub struct Range {
150 pub lower: Option<Value>,
151 pub upper: Option<Value>,
152 pub inc_lower: bool,
153 pub inc_upper: bool,
154 pub empty: bool,
155}
156
157/// Result of a `group` statement: one grouping key, the grouping label
158/// list, and the elements sharing that key.
159#[derive(Debug, Clone, PartialEq)]
160pub struct Group {
161 pub key: Object,
162 pub grouping: Vec<String>,
163 pub elements: Vec<Value>,
164}