Skip to main content

pylon_client/
query_arg.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//! Query arguments: [`QueryArg`] for a single value, [`QueryArgs`] for the
21//! collection a query method takes, and [`ValueOpt`] + [`named_args!`] for
22//! building a named collection inline.
23//!
24//! Conventional enough in its surface that a call site arriving from another
25//! Rust client keeps its argument expressions: `&()` for no arguments,
26//! `&(a, b)` for positional `$0`/`$1`, and
27//! `&named_args! { "name" => value }` for `$name`.
28//!
29//! Positional arguments work because PyQL compiles `$0` to the parameter
30//! *name* `"0"` — so a tuple is just a named collection whose names are its
31//! indices.
32
33use std::collections::HashMap;
34
35use pylon_value::DecodedValue;
36
37/// A single query argument. Implemented for the scalars Pylon can bind, for
38/// `Option`/the common `Vec` element types, and for [`DecodedValue`] itself
39/// as the escape hatch for anything not covered here.
40pub trait QueryArg {
41    fn to_decoded(&self) -> DecodedValue;
42}
43
44impl<T: QueryArg + ?Sized> QueryArg for &T {
45    fn to_decoded(&self) -> DecodedValue {
46        (**self).to_decoded()
47    }
48}
49
50impl QueryArg for DecodedValue {
51    fn to_decoded(&self) -> DecodedValue {
52        self.clone()
53    }
54}
55
56/// An absent optional argument (`<optional str>$x`) binds as NULL.
57impl<T: QueryArg> QueryArg for Option<T> {
58    fn to_decoded(&self) -> DecodedValue {
59        match self {
60            Some(value) => value.to_decoded(),
61            None => DecodedValue::Null,
62        }
63    }
64}
65
66macro_rules! query_arg_via_into {
67    ($($target:ty),* $(,)?) => {
68        $(
69            impl QueryArg for $target {
70                fn to_decoded(&self) -> DecodedValue {
71                    DecodedValue::from(self.clone())
72                }
73            }
74        )*
75    };
76}
77
78query_arg_via_into!(bool, i16, i32, i64, f32, f64, String, uuid::Uuid);
79
80impl QueryArg for str {
81    fn to_decoded(&self) -> DecodedValue {
82        DecodedValue::Str(self.to_string())
83    }
84}
85
86/// `Vec<u8>` is bytes, not an array of integers — matching
87/// `DecodedValue::from(Vec<u8>)`, which takes precedence. That is also why
88/// the array impls below are enumerated per element type instead of a
89/// blanket `impl<T: QueryArg> QueryArg for Vec<T>`: the blanket form would
90/// overlap this one, and Rust won't accept the pair on the grounds that
91/// `u8` merely happens not to implement `QueryArg` today.
92impl QueryArg for Vec<u8> {
93    fn to_decoded(&self) -> DecodedValue {
94        DecodedValue::Bytes(self.clone())
95    }
96}
97
98macro_rules! query_arg_array {
99    ($($element:ty),* $(,)?) => {
100        $(
101            impl QueryArg for Vec<$element> {
102                fn to_decoded(&self) -> DecodedValue {
103                    DecodedValue::Array(self.iter().map(QueryArg::to_decoded).collect())
104                }
105            }
106
107            impl QueryArg for [$element] {
108                fn to_decoded(&self) -> DecodedValue {
109                    DecodedValue::Array(self.iter().map(QueryArg::to_decoded).collect())
110                }
111            }
112        )*
113    };
114}
115
116query_arg_array!(bool, i16, i32, i64, f32, f64, String, uuid::Uuid);
117
118/// `std::datetime`. Pylon stores a timestamp as microseconds from the
119/// PostgreSQL epoch (2000-01-01), not the Unix epoch.
120impl QueryArg for chrono::DateTime<chrono::Utc> {
121    fn to_decoded(&self) -> DecodedValue {
122        DecodedValue::Timestamptz(pg_micros(self.naive_utc()))
123    }
124}
125
126/// `cal::local_datetime`.
127impl QueryArg for chrono::NaiveDateTime {
128    fn to_decoded(&self) -> DecodedValue {
129        DecodedValue::Timestamp(pg_micros(*self))
130    }
131}
132
133/// `cal::local_date`.
134impl QueryArg for chrono::NaiveDate {
135    fn to_decoded(&self) -> DecodedValue {
136        DecodedValue::Date(self.signed_duration_since(pg_epoch_date()).num_days() as i32)
137    }
138}
139
140/// `cal::local_time`.
141impl QueryArg for chrono::NaiveTime {
142    fn to_decoded(&self) -> DecodedValue {
143        let midnight = chrono::NaiveTime::from_hms_opt(0, 0, 0).expect("00:00:00 is a valid time");
144        DecodedValue::Time(
145            self.signed_duration_since(midnight)
146                .num_microseconds()
147                .unwrap_or_default(),
148        )
149    }
150}
151
152/// `std::duration`. Pylon stores an interval as its three wire components;
153/// months is always 0 here, since a `std::time::Duration` is a fixed span and
154/// a calendar month is not (`cal::relative_duration` is the type that carries
155/// one). Days stay 0 too — Postgres treats an interval's days as
156/// calendar-relative across a DST boundary, so the whole span goes in
157/// microseconds.
158impl QueryArg for std::time::Duration {
159    fn to_decoded(&self) -> DecodedValue {
160        DecodedValue::Interval {
161            months: 0,
162            days: 0,
163            microseconds: i64::try_from(self.as_micros()).unwrap_or(i64::MAX),
164        }
165    }
166}
167
168/// A `json` argument. Bound as JSON text rather than as a
169/// `DecodedValue::Object`, because that is the form `pylon-pgcon` accepts
170/// for a `jsonb` parameter regardless of whether the document's root is an
171/// object, and it saves every call site the
172/// `serde_json::to_string(&value)?` it would otherwise write by hand.
173impl QueryArg for serde_json::Value {
174    fn to_decoded(&self) -> DecodedValue {
175        DecodedValue::Str(self.to_string())
176    }
177}
178
179fn pg_epoch_date() -> chrono::NaiveDate {
180    chrono::NaiveDate::from_ymd_opt(2000, 1, 1).expect("2000-01-01 is a valid date")
181}
182
183/// Microseconds from the PostgreSQL epoch. Saturates rather than wrapping
184/// for a datetime far enough out to overflow — a value that extreme is a
185/// caller bug, and a silently wrapped timestamp would be worse than a
186/// clamped one.
187fn pg_micros(value: chrono::NaiveDateTime) -> i64 {
188    let epoch = pg_epoch_date().and_hms_opt(0, 0, 0).expect("00:00:00 is a valid time");
189    value
190        .signed_duration_since(epoch)
191        .num_microseconds()
192        .unwrap_or(i64::MAX)
193}
194
195/// An argument value inside [`named_args!`], constructible from anything
196/// that is a [`QueryArg`]. The reason `named_args!` can mix argument types in
197/// one collection.
198#[derive(Debug, Clone, PartialEq)]
199pub struct ValueOpt(DecodedValue);
200
201impl<T: QueryArg> From<T> for ValueOpt {
202    fn from(value: T) -> Self {
203        ValueOpt(value.to_decoded())
204    }
205}
206
207impl ValueOpt {
208    pub fn into_decoded(self) -> DecodedValue {
209        self.0
210    }
211}
212
213impl From<ValueOpt> for DecodedValue {
214    fn from(value: ValueOpt) -> Self {
215        value.0
216    }
217}
218
219/// The collection of arguments a query method takes. `&()` when there are
220/// none, a tuple for positional `$0`/`$1`/…, a `HashMap` or a slice of
221/// `(name, value)` pairs for named ones.
222pub trait QueryArgs {
223    /// Borrows the names out of `self` (which the query method holds by
224    /// reference for the whole call) so only the values are cloned.
225    fn to_params(&self) -> Vec<(&str, DecodedValue)>;
226}
227
228impl QueryArgs for () {
229    fn to_params(&self) -> Vec<(&str, DecodedValue)> {
230        Vec::new()
231    }
232}
233
234impl QueryArgs for [(&str, DecodedValue)] {
235    fn to_params(&self) -> Vec<(&str, DecodedValue)> {
236        self.iter().map(|(name, value)| (*name, value.clone())).collect()
237    }
238}
239
240impl<const N: usize> QueryArgs for [(&str, DecodedValue); N] {
241    fn to_params(&self) -> Vec<(&str, DecodedValue)> {
242        self.as_slice().to_params()
243    }
244}
245
246impl QueryArgs for Vec<(&str, DecodedValue)> {
247    fn to_params(&self) -> Vec<(&str, DecodedValue)> {
248        self.as_slice().to_params()
249    }
250}
251
252/// Keyed by [`ValueOpt`] rather than by any `V: QueryArg`, because
253/// `ValueOpt` deliberately does *not* implement `QueryArg`: it is built from
254/// one via a blanket `From`, and making it a `QueryArg` too would collide
255/// with the standard library's reflexive `impl<T> From<T> for T`.
256impl QueryArgs for HashMap<&str, ValueOpt> {
257    fn to_params(&self) -> Vec<(&str, DecodedValue)> {
258        self.iter().map(|(name, value)| (*name, value.0.clone())).collect()
259    }
260}
261
262impl QueryArgs for HashMap<String, ValueOpt> {
263    fn to_params(&self) -> Vec<(&str, DecodedValue)> {
264        self.iter()
265            .map(|(name, value)| (name.as_str(), value.0.clone()))
266            .collect()
267    }
268}
269
270/// `$0`, `$1`, … compile to these parameter names, so a positional tuple
271/// needs no allocation to name its own elements.
272const POSITIONAL_NAMES: [&str; 12] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"];
273
274macro_rules! impl_query_args_for_tuple {
275    ($($index:tt : $param:ident),+) => {
276        impl<$($param: QueryArg),+> QueryArgs for ($($param,)+) {
277            fn to_params(&self) -> Vec<(&str, DecodedValue)> {
278                vec![$((POSITIONAL_NAMES[$index], self.$index.to_decoded())),+]
279            }
280        }
281    };
282}
283
284impl_query_args_for_tuple!(0: A);
285impl_query_args_for_tuple!(0: A, 1: B);
286impl_query_args_for_tuple!(0: A, 1: B, 2: C);
287impl_query_args_for_tuple!(0: A, 1: B, 2: C, 3: D);
288impl_query_args_for_tuple!(0: A, 1: B, 2: C, 3: D, 4: E);
289impl_query_args_for_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F);
290impl_query_args_for_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G);
291impl_query_args_for_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H);
292impl_query_args_for_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I);
293impl_query_args_for_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J);
294impl_query_args_for_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K);
295impl_query_args_for_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K, 11: L);
296
297/// Builds a named argument collection for a query with `$name` parameters:
298///
299/// ```no_run
300/// # use pylon_client::named_args;
301/// # fn go(client: &pylon_client::Client, id: uuid::Uuid) {
302/// let args = named_args! { "id" => id, "label" => "urgent".to_string() };
303/// # let _ = (client, args);
304/// # }
305/// ```
306///
307/// Builds a `HashMap<&str, ValueOpt>`, with a trailing comma allowed.
308#[macro_export]
309macro_rules! named_args {
310    ($($key:expr => $value:expr,)+) => { $crate::named_args!($($key => $value),+) };
311    ($($key:expr => $value:expr),*) => {{
312        let mut args = ::std::collections::HashMap::<&str, $crate::ValueOpt>::new();
313        $(
314            args.insert($key, $crate::ValueOpt::from($value));
315        )*
316        args
317    }};
318}