Skip to main content

reddb_client/
params.rs

1//! Driver-side parameter values for `query_with(sql, &[Value])`.
2//!
3//! Tracer-bullet implementation of issue #364 (Rust leg of PRD #351).
4//! Mirrors the same `Value` taxonomy the Go (`drivers/go/redwire/value.go`)
5//! and JS (`drivers/js/src/redwire.js`) drivers ship: 10 variants that map
6//! 1:1 to the engine's binder slots through `reddb_server::storage::schema::Value`.
7//!
8//! Deep module pattern: this module owns *only* parameter serialization.
9//! Transports import it; they don't reimplement type mapping. Two conversions
10//! are exposed:
11//!
12//! - [`Value::into_json_param`] → `serde_json::Value` for HTTP (POST /query)
13//!   and any future JSON-RPC transport. Uses the `{"$bytes": ...}` / `{"$ts":
14//!   ...}` / `{"$uuid": ...}` envelope agreed in ADR 0001 / PRD #351.
15//! - [`Value::into_schema_value`] (cfg `embedded`) → `SchemaValue` for the
16//!   in-process binder. Avoids JSON round-trip on the hot embedded path.
17//!
18//! `IntoValue` covers the natural Rust → `Value` conversions called out by
19//! the issue: primitives, `Vec<f32>`, `&[u8]`, `serde_json::Value`,
20//! `chrono::DateTime` (when `chrono` is in the caller's deps — we accept a
21//! plain `i64` seconds-since-epoch so we don't force a chrono dep on the
22//! client crate), and `Uuid` (16-byte raw form — callers using the `uuid`
23//! crate convert via `Uuid::as_bytes`).
24
25use crate::types::JsonValue;
26
27/// One parameter value for a `query_with(sql, params)` call.
28///
29/// Variants mirror the engine's binder slots; see
30/// `crates/reddb-server/src/storage/query/user_params.rs` for the
31/// authoritative per-slot type rules.
32#[derive(Debug, Clone, PartialEq)]
33pub enum Value {
34    Null,
35    Bool(bool),
36    Int(i64),
37    Float(f64),
38    Text(String),
39    Bytes(Vec<u8>),
40    /// f32 vector for similarity / vector slots.
41    Vector(Vec<f32>),
42    /// Structured JSON (object / array / scalar).
43    Json(JsonValue),
44    /// Seconds since Unix epoch.
45    Timestamp(i64),
46    /// Raw 16-byte UUID.
47    Uuid([u8; 16]),
48}
49
50impl Value {
51    /// Compatibility constructor for issue #386 examples and callers coming
52    /// from drivers that spell the integer wire value as `Int64`.
53    #[allow(non_snake_case)]
54    pub fn Int64(value: i64) -> Self {
55        Self::Int(value)
56    }
57
58    /// Canonical JSON envelope used by HTTP `POST /query`'s `params`
59    /// field and any future JSON-RPC transport. Matches the shape
60    /// `crates/reddb-server/src/server/handlers_query.rs` accepts.
61    pub fn into_json_param(self) -> serde_json::Value {
62        match self {
63            Value::Null => serde_json::Value::Null,
64            Value::Bool(b) => serde_json::Value::Bool(b),
65            Value::Int(n) => serde_json::Value::Number(n.into()),
66            Value::Float(n) => serde_json::Number::from_f64(n)
67                .map(serde_json::Value::Number)
68                .unwrap_or(serde_json::Value::Null),
69            Value::Text(s) => serde_json::Value::String(s),
70            Value::Bytes(b) => serde_json::json!({ "$bytes": base64_encode(&b) }),
71            Value::Vector(v) => serde_json::Value::Array(
72                v.into_iter()
73                    .map(|f| {
74                        serde_json::Number::from_f64(f as f64)
75                            .map(serde_json::Value::Number)
76                            .unwrap_or(serde_json::Value::Null)
77                    })
78                    .collect(),
79            ),
80            Value::Json(j) => json_to_serde(&j),
81            Value::Timestamp(secs) => serde_json::json!({ "$ts": secs }),
82            Value::Uuid(bytes) => serde_json::json!({ "$uuid": format_uuid(&bytes) }),
83        }
84    }
85
86    /// In-process conversion to the engine's `SchemaValue`. Skips JSON
87    /// round-trip on the embedded path. Available only when the crate
88    /// is built with `embedded`.
89    #[cfg(feature = "embedded")]
90    pub fn into_schema_value(self) -> reddb_server::storage::schema::Value {
91        use reddb_server::storage::schema::Value as SV;
92        match self {
93            Value::Null => SV::Null,
94            Value::Bool(b) => SV::Boolean(b),
95            Value::Int(n) => SV::Integer(n),
96            Value::Float(n) => SV::Float(n),
97            Value::Text(s) => SV::Text(std::sync::Arc::from(s.as_str())),
98            Value::Bytes(b) => SV::Blob(b),
99            Value::Vector(v) => SV::Vector(v),
100            Value::Json(j) => SV::Json(j.to_json_string().into_bytes()),
101            Value::Timestamp(secs) => SV::Timestamp(secs),
102            Value::Uuid(bytes) => SV::Uuid(bytes),
103        }
104    }
105}
106
107/// Ergonomic conversions so callers can write
108/// `db.query_with(sql, &[42i64.into(), "alice".into()])`.
109pub trait IntoValue {
110    fn into_value(self) -> Value;
111}
112
113impl IntoValue for Value {
114    fn into_value(self) -> Value {
115        self
116    }
117}
118
119impl IntoValue for bool {
120    fn into_value(self) -> Value {
121        Value::Bool(self)
122    }
123}
124
125macro_rules! int_into_value {
126    ($($t:ty),*) => {
127        $(
128            impl IntoValue for $t {
129                fn into_value(self) -> Value { Value::Int(self as i64) }
130            }
131        )*
132    };
133}
134int_into_value!(i8, i16, i32, i64, u8, u16, u32);
135
136impl IntoValue for u64 {
137    fn into_value(self) -> Value {
138        // u64 > i64::MAX is currently out of band — match Go driver's
139        // overflow contract and surface it as a runtime error at
140        // serialize time rather than silently wrapping. The simplest
141        // path is to clamp via `try_from` and panic on overflow; the
142        // typed `query_with` API takes already-built `Value`s, so the
143        // caller can route through `Value::Int` directly when they need
144        // explicit handling.
145        Value::Int(i64::try_from(self).expect("u64 param > i64::MAX"))
146    }
147}
148
149impl IntoValue for f32 {
150    fn into_value(self) -> Value {
151        Value::Float(self as f64)
152    }
153}
154
155impl IntoValue for f64 {
156    fn into_value(self) -> Value {
157        Value::Float(self)
158    }
159}
160
161impl IntoValue for &str {
162    fn into_value(self) -> Value {
163        Value::Text(self.to_string())
164    }
165}
166
167impl IntoValue for String {
168    fn into_value(self) -> Value {
169        Value::Text(self)
170    }
171}
172
173impl IntoValue for Vec<u8> {
174    fn into_value(self) -> Value {
175        Value::Bytes(self)
176    }
177}
178
179impl IntoValue for &[u8] {
180    fn into_value(self) -> Value {
181        Value::Bytes(self.to_vec())
182    }
183}
184
185impl IntoValue for Vec<f32> {
186    fn into_value(self) -> Value {
187        Value::Vector(self)
188    }
189}
190
191impl IntoValue for &[f32] {
192    fn into_value(self) -> Value {
193        Value::Vector(self.to_vec())
194    }
195}
196
197impl IntoValue for serde_json::Value {
198    fn into_value(self) -> Value {
199        Value::Json(serde_to_json(&self))
200    }
201}
202
203impl IntoValue for JsonValue {
204    fn into_value(self) -> Value {
205        Value::Json(self)
206    }
207}
208
209impl<T: IntoValue> IntoValue for Option<T> {
210    fn into_value(self) -> Value {
211        match self {
212            Some(v) => v.into_value(),
213            None => Value::Null,
214        }
215    }
216}
217
218/// Convert user-facing parameter containers into the driver Value list.
219///
220/// This trait is sealed so the accepted parameter shapes remain explicit and
221/// consistent across transports.
222pub trait IntoParams: sealed::Sealed {
223    fn into_params(self) -> Vec<Value>;
224}
225
226mod sealed {
227    pub trait Sealed {}
228}
229
230impl sealed::Sealed for () {}
231
232impl IntoParams for () {
233    fn into_params(self) -> Vec<Value> {
234        Vec::new()
235    }
236}
237
238impl<V: IntoValue> sealed::Sealed for Vec<V> {}
239
240impl<V: IntoValue> IntoParams for Vec<V> {
241    fn into_params(self) -> Vec<Value> {
242        self.into_iter().map(IntoValue::into_value).collect()
243    }
244}
245
246impl<V: IntoValue + Clone> sealed::Sealed for &[V] {}
247
248impl<V: IntoValue + Clone> IntoParams for &[V] {
249    fn into_params(self) -> Vec<Value> {
250        self.iter().cloned().map(IntoValue::into_value).collect()
251    }
252}
253
254impl<V: IntoValue + Clone> sealed::Sealed for &Vec<V> {}
255
256impl<V: IntoValue + Clone> IntoParams for &Vec<V> {
257    fn into_params(self) -> Vec<Value> {
258        self.as_slice().into_params()
259    }
260}
261
262impl<V: IntoValue + Clone, const N: usize> sealed::Sealed for &[V; N] {}
263
264impl<V: IntoValue + Clone, const N: usize> IntoParams for &[V; N] {
265    fn into_params(self) -> Vec<Value> {
266        self.as_slice().into_params()
267    }
268}
269
270impl<V: IntoValue, const N: usize> sealed::Sealed for [V; N] {}
271
272impl<V: IntoValue, const N: usize> IntoParams for [V; N] {
273    fn into_params(self) -> Vec<Value> {
274        self.into_iter().map(IntoValue::into_value).collect()
275    }
276}
277
278macro_rules! tuple_into_params {
279    ($($name:ident),+) => {
280        impl<$($name: IntoValue),+> sealed::Sealed for ($($name,)+) {}
281
282        impl<$($name: IntoValue),+> IntoParams for ($($name,)+) {
283            #[allow(non_snake_case)]
284            fn into_params(self) -> Vec<Value> {
285                let ($($name,)+) = self;
286                vec![$($name.into_value()),+]
287            }
288        }
289    };
290}
291
292tuple_into_params!(A);
293tuple_into_params!(A, B);
294tuple_into_params!(A, B, C);
295tuple_into_params!(A, B, C, D);
296tuple_into_params!(A, B, C, D, E);
297tuple_into_params!(A, B, C, D, E, F);
298tuple_into_params!(A, B, C, D, E, F, G);
299tuple_into_params!(A, B, C, D, E, F, G, H);
300
301fn json_to_serde(v: &JsonValue) -> serde_json::Value {
302    match v {
303        JsonValue::Null => serde_json::Value::Null,
304        JsonValue::Bool(b) => serde_json::Value::Bool(*b),
305        JsonValue::Number(n) => serde_json::Number::from_f64(*n)
306            .map(serde_json::Value::Number)
307            .unwrap_or(serde_json::Value::Null),
308        JsonValue::String(s) => serde_json::Value::String(s.clone()),
309        JsonValue::Array(items) => {
310            serde_json::Value::Array(items.iter().map(json_to_serde).collect())
311        }
312        JsonValue::Object(entries) => {
313            let mut map = serde_json::Map::with_capacity(entries.len());
314            for (k, v) in entries {
315                map.insert(k.clone(), json_to_serde(v));
316            }
317            serde_json::Value::Object(map)
318        }
319    }
320}
321
322fn serde_to_json(v: &serde_json::Value) -> JsonValue {
323    match v {
324        serde_json::Value::Null => JsonValue::Null,
325        serde_json::Value::Bool(b) => JsonValue::Bool(*b),
326        serde_json::Value::Number(n) => JsonValue::Number(n.as_f64().unwrap_or(0.0)),
327        serde_json::Value::String(s) => JsonValue::String(s.clone()),
328        serde_json::Value::Array(items) => {
329            JsonValue::Array(items.iter().map(serde_to_json).collect())
330        }
331        serde_json::Value::Object(map) => JsonValue::Object(
332            map.iter()
333                .map(|(k, v)| (k.clone(), serde_to_json(v)))
334                .collect(),
335        ),
336    }
337}
338
339fn base64_encode(bytes: &[u8]) -> String {
340    const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
341    let mut out = String::with_capacity((bytes.len() + 2) / 3 * 4);
342    let mut chunks = bytes.chunks_exact(3);
343    for c in chunks.by_ref() {
344        let n = ((c[0] as u32) << 16) | ((c[1] as u32) << 8) | (c[2] as u32);
345        out.push(TABLE[((n >> 18) & 0x3F) as usize] as char);
346        out.push(TABLE[((n >> 12) & 0x3F) as usize] as char);
347        out.push(TABLE[((n >> 6) & 0x3F) as usize] as char);
348        out.push(TABLE[(n & 0x3F) as usize] as char);
349    }
350    let rem = chunks.remainder();
351    match rem.len() {
352        0 => {}
353        1 => {
354            let n = (rem[0] as u32) << 16;
355            out.push(TABLE[((n >> 18) & 0x3F) as usize] as char);
356            out.push(TABLE[((n >> 12) & 0x3F) as usize] as char);
357            out.push('=');
358            out.push('=');
359        }
360        2 => {
361            let n = ((rem[0] as u32) << 16) | ((rem[1] as u32) << 8);
362            out.push(TABLE[((n >> 18) & 0x3F) as usize] as char);
363            out.push(TABLE[((n >> 12) & 0x3F) as usize] as char);
364            out.push(TABLE[((n >> 6) & 0x3F) as usize] as char);
365            out.push('=');
366        }
367        _ => unreachable!(),
368    }
369    out
370}
371
372fn format_uuid(bytes: &[u8; 16]) -> String {
373    let mut out = String::with_capacity(36);
374    for (i, b) in bytes.iter().enumerate() {
375        if matches!(i, 4 | 6 | 8 | 10) {
376            out.push('-');
377        }
378        out.push_str(&format!("{b:02x}"));
379    }
380    out
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386
387    #[test]
388    fn into_value_primitives() {
389        assert_eq!(true.into_value(), Value::Bool(true));
390        assert_eq!(42i32.into_value(), Value::Int(42));
391        assert_eq!((-1i64).into_value(), Value::Int(-1));
392        assert_eq!(2.5f64.into_value(), Value::Float(2.5));
393        assert_eq!("hi".into_value(), Value::Text("hi".to_string()));
394        assert_eq!(String::from("x").into_value(), Value::Text("x".to_string()));
395    }
396
397    #[test]
398    fn into_value_bytes_and_vector() {
399        assert_eq!(vec![1u8, 2, 3].into_value(), Value::Bytes(vec![1, 2, 3]));
400        let slice: &[u8] = &[9, 8];
401        assert_eq!(slice.into_value(), Value::Bytes(vec![9, 8]));
402        assert_eq!(
403            vec![0.1f32, 0.2].into_value(),
404            Value::Vector(vec![0.1, 0.2])
405        );
406    }
407
408    #[test]
409    fn into_value_option_maps_to_null() {
410        let none: Option<i64> = None;
411        assert_eq!(none.into_value(), Value::Null);
412        let some: Option<i64> = Some(7);
413        assert_eq!(some.into_value(), Value::Int(7));
414    }
415
416    #[test]
417    fn json_param_envelope_for_bytes() {
418        let v = Value::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]);
419        let j = v.into_json_param();
420        assert_eq!(j["$bytes"].as_str().unwrap(), "3q2+7w==");
421    }
422
423    #[test]
424    fn json_param_envelope_for_timestamp() {
425        let j = Value::Timestamp(1_700_000_000).into_json_param();
426        assert_eq!(j["$ts"].as_i64().unwrap(), 1_700_000_000);
427    }
428
429    #[test]
430    fn json_param_envelope_for_uuid() {
431        let bytes = [
432            0x55, 0x0e, 0x84, 0x00, 0xe2, 0x9b, 0x41, 0xd4, 0xa7, 0x16, 0x44, 0x66, 0x55, 0x44,
433            0x00, 0x00,
434        ];
435        let j = Value::Uuid(bytes).into_json_param();
436        assert_eq!(
437            j["$uuid"].as_str().unwrap(),
438            "550e8400-e29b-41d4-a716-446655440000"
439        );
440    }
441
442    #[test]
443    fn json_param_vector_is_plain_array() {
444        let j = Value::Vector(vec![0.0, 1.0, -1.5]).into_json_param();
445        let arr = j.as_array().unwrap();
446        assert_eq!(arr.len(), 3);
447        assert!((arr[0].as_f64().unwrap() - 0.0).abs() < 1e-6);
448        assert!((arr[2].as_f64().unwrap() - -1.5).abs() < 1e-6);
449    }
450
451    #[test]
452    fn base64_encode_known_vectors() {
453        // RFC 4648 §10 test vectors.
454        assert_eq!(base64_encode(b""), "");
455        assert_eq!(base64_encode(b"f"), "Zg==");
456        assert_eq!(base64_encode(b"fo"), "Zm8=");
457        assert_eq!(base64_encode(b"foo"), "Zm9v");
458        assert_eq!(base64_encode(b"foob"), "Zm9vYg==");
459        assert_eq!(base64_encode(b"fooba"), "Zm9vYmE=");
460        assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
461    }
462
463    #[cfg(feature = "embedded")]
464    #[test]
465    fn into_schema_value_covers_all_variants() {
466        use reddb_server::storage::schema::Value as SV;
467        assert!(matches!(Value::Null.into_schema_value(), SV::Null));
468        assert!(matches!(
469            Value::Bool(true).into_schema_value(),
470            SV::Boolean(true)
471        ));
472        assert!(matches!(Value::Int(7).into_schema_value(), SV::Integer(7)));
473        assert!(
474            matches!(Value::Float(1.5).into_schema_value(), SV::Float(f) if (f - 1.5).abs() < 1e-9)
475        );
476        let SV::Text(s) = Value::Text("x".into()).into_schema_value() else {
477            panic!()
478        };
479        assert_eq!(s.as_ref(), "x");
480        assert!(
481            matches!(Value::Bytes(vec![1, 2]).into_schema_value(), SV::Blob(b) if b == vec![1, 2])
482        );
483        assert!(matches!(
484            Value::Vector(vec![0.1, 0.2]).into_schema_value(),
485            SV::Vector(v) if v == vec![0.1f32, 0.2]
486        ));
487        assert!(matches!(
488            Value::Timestamp(99).into_schema_value(),
489            SV::Timestamp(99)
490        ));
491        let SV::Uuid(b) = Value::Uuid([0u8; 16]).into_schema_value() else {
492            panic!()
493        };
494        assert_eq!(b, [0u8; 16]);
495    }
496}