Skip to main content

radixdb_api/
params.rs

1// Copyright 2026 RadixDB Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Parameter binding for SQL queries
16//!
17//! This module provides ergonomic parameter passing for SQL queries,
18//! similar to rusqlite's `params!` macro.
19//!
20//! # Examples
21//!
22//! ```no_run
23//! use radixdb_api::{Database, params, named_params};
24//! # fn main() -> radixdb_core::Result<()> {
25//!
26//! let db = Database::open("memory://")?;
27//!
28//! // Using params! macro (positional)
29//! db.execute("INSERT INTO users VALUES ($1, $2, $3)", params![1, "Alice", 30])?;
30//!
31//! // Using tuple syntax (positional)
32//! db.execute("INSERT INTO users VALUES ($1, $2)", (1, "Alice"))?;
33//!
34//! // Using named_params! macro
35//! db.execute_named(
36//!     "INSERT INTO users VALUES (:id, :name, :age)",
37//!     named_params!{ id: 1, name: "Alice", age: 30 }
38//! )?;
39//! # Ok(())
40//! # }
41//! ```
42
43use rustc_hash::FxHashMap;
44use std::collections::HashMap;
45use std::sync::Arc;
46
47use chrono::{DateTime, NaiveDate, Utc};
48use radixdb_core::SmartString;
49use radixdb_core::{Result, Value};
50
51pub use radixdb_core::ParamVec;
52
53/// Exact public representation of a SQL DECIMAL value.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub struct DecimalValue {
56    unscaled: i128,
57    precision: u8,
58    scale: u8,
59}
60
61impl DecimalValue {
62    /// Construct a validated DECIMAL payload.
63    pub fn try_new(unscaled: i128, precision: u8, scale: u8) -> Result<Self> {
64        Value::try_decimal(unscaled, precision, scale)?;
65        Ok(Self {
66            unscaled,
67            precision,
68            scale,
69        })
70    }
71
72    pub fn unscaled(self) -> i128 {
73        self.unscaled
74    }
75
76    pub fn precision(self) -> u8 {
77        self.precision
78    }
79
80    pub fn scale(self) -> u8 {
81        self.scale
82    }
83}
84
85/// Trait for types that can be converted to SQL parameters
86///
87/// This trait is automatically implemented for common Rust types.
88/// It enables the `params!` macro and tuple parameter syntax.
89///
90/// Platform-sized unsigned integers require an explicit checked conversion,
91/// because RadixDB integer parameters use the signed `i64` domain:
92///
93/// ```compile_fail
94/// use radixdb::ToParam;
95///
96/// let _ = usize::MAX.to_param();
97/// ```
98pub trait ToParam {
99    /// Convert self into a Value for SQL parameter binding
100    fn to_param(&self) -> Value;
101}
102
103// Implement ToParam for common types
104
105impl ToParam for i64 {
106    fn to_param(&self) -> Value {
107        Value::Integer(*self)
108    }
109}
110
111impl ToParam for i32 {
112    fn to_param(&self) -> Value {
113        Value::Integer(*self as i64)
114    }
115}
116
117impl ToParam for i16 {
118    fn to_param(&self) -> Value {
119        Value::Integer(*self as i64)
120    }
121}
122
123impl ToParam for i8 {
124    fn to_param(&self) -> Value {
125        Value::Integer(*self as i64)
126    }
127}
128
129impl ToParam for u32 {
130    fn to_param(&self) -> Value {
131        Value::Integer(*self as i64)
132    }
133}
134
135impl ToParam for u16 {
136    fn to_param(&self) -> Value {
137        Value::Integer(*self as i64)
138    }
139}
140
141impl ToParam for u8 {
142    fn to_param(&self) -> Value {
143        Value::Integer(*self as i64)
144    }
145}
146
147impl ToParam for f64 {
148    fn to_param(&self) -> Value {
149        Value::Float(*self)
150    }
151}
152
153impl ToParam for f32 {
154    fn to_param(&self) -> Value {
155        Value::Float(*self as f64)
156    }
157}
158
159impl ToParam for bool {
160    fn to_param(&self) -> Value {
161        Value::Boolean(*self)
162    }
163}
164
165impl ToParam for String {
166    fn to_param(&self) -> Value {
167        Value::Text(SmartString::new(self))
168    }
169}
170
171impl ToParam for &str {
172    fn to_param(&self) -> Value {
173        Value::Text(SmartString::from(*self))
174    }
175}
176
177impl ToParam for Arc<str> {
178    fn to_param(&self) -> Value {
179        Value::Text(SmartString::from(Arc::clone(self)))
180    }
181}
182
183impl ToParam for DateTime<Utc> {
184    fn to_param(&self) -> Value {
185        Value::Timestamp(*self)
186    }
187}
188
189impl ToParam for NaiveDate {
190    fn to_param(&self) -> Value {
191        let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).expect("valid Unix epoch date");
192        let days = self.signed_duration_since(epoch).num_days();
193        Value::date(i32::try_from(days).expect("chrono date range fits SQL DATE domain"))
194    }
195}
196
197impl ToParam for Vec<u8> {
198    fn to_param(&self) -> Value {
199        Value::bytes(self.clone())
200    }
201}
202
203impl ToParam for Vec<f32> {
204    fn to_param(&self) -> Value {
205        Value::vector(self.clone())
206    }
207}
208
209impl ToParam for uuid::Uuid {
210    fn to_param(&self) -> Value {
211        Value::uuid(*self.as_bytes())
212    }
213}
214
215impl ToParam for serde_json::Value {
216    fn to_param(&self) -> Value {
217        Value::try_json(self.to_string()).expect("serialized serde_json::Value is valid JSON")
218    }
219}
220
221impl ToParam for DecimalValue {
222    fn to_param(&self) -> Value {
223        Value::try_decimal(self.unscaled, self.precision, self.scale)
224            .expect("DecimalValue validates its invariant at construction")
225    }
226}
227
228impl ToParam for Value {
229    fn to_param(&self) -> Value {
230        self.clone()
231    }
232}
233
234impl<T: ToParam> ToParam for Option<T> {
235    fn to_param(&self) -> Value {
236        match self {
237            Some(v) => v.to_param(),
238            None => Value::null_unknown(),
239        }
240    }
241}
242
243impl<T: ToParam> ToParam for &T {
244    fn to_param(&self) -> Value {
245        (*self).to_param()
246    }
247}
248
249/// Trait for collections of parameters
250///
251/// This enables passing tuples, arrays, and slices as parameters.
252pub trait Params {
253    /// Convert into a ParamVec of Values.
254    /// Uses SmallVec to avoid heap allocation for ≤4 parameters.
255    fn into_params(self) -> ParamVec;
256}
257
258// Empty params
259impl Params for () {
260    fn into_params(self) -> ParamVec {
261        ParamVec::new()
262    }
263}
264
265// Slice of Values
266impl Params for &[Value] {
267    fn into_params(self) -> ParamVec {
268        self.iter().cloned().collect()
269    }
270}
271
272// Vec of Values
273impl Params for Vec<Value> {
274    fn into_params(self) -> ParamVec {
275        self.into_iter().collect()
276    }
277}
278
279// ParamVec (SmallVec) — zero-cost passthrough, no conversion needed
280impl Params for ParamVec {
281    fn into_params(self) -> ParamVec {
282        self
283    }
284}
285
286// Array of Values
287impl<const N: usize> Params for [Value; N] {
288    fn into_params(self) -> ParamVec {
289        self.into_iter().collect()
290    }
291}
292
293// Tuple implementations for 1-12 elements
294macro_rules! impl_params_for_tuple {
295    ($($idx:tt: $T:ident),+) => {
296        impl<$($T: ToParam),+> Params for ($($T,)+) {
297            fn into_params(self) -> ParamVec {
298                smallvec::smallvec![$(self.$idx.to_param()),+]
299            }
300        }
301    };
302}
303
304impl_params_for_tuple!(0: T0);
305impl_params_for_tuple!(0: T0, 1: T1);
306impl_params_for_tuple!(0: T0, 1: T1, 2: T2);
307impl_params_for_tuple!(0: T0, 1: T1, 2: T2, 3: T3);
308impl_params_for_tuple!(0: T0, 1: T1, 2: T2, 3: T3, 4: T4);
309impl_params_for_tuple!(0: T0, 1: T1, 2: T2, 3: T3, 4: T4, 5: T5);
310impl_params_for_tuple!(0: T0, 1: T1, 2: T2, 3: T3, 4: T4, 5: T5, 6: T6);
311impl_params_for_tuple!(0: T0, 1: T1, 2: T2, 3: T3, 4: T4, 5: T5, 6: T6, 7: T7);
312impl_params_for_tuple!(0: T0, 1: T1, 2: T2, 3: T3, 4: T4, 5: T5, 6: T6, 7: T7, 8: T8);
313impl_params_for_tuple!(0: T0, 1: T1, 2: T2, 3: T3, 4: T4, 5: T5, 6: T6, 7: T7, 8: T8, 9: T9);
314impl_params_for_tuple!(0: T0, 1: T1, 2: T2, 3: T3, 4: T4, 5: T5, 6: T6, 7: T7, 8: T8, 9: T9, 10: T10);
315impl_params_for_tuple!(0: T0, 1: T1, 2: T2, 3: T3, 4: T4, 5: T5, 6: T6, 7: T7, 8: T8, 9: T9, 10: T10, 11: T11);
316
317/// Create a parameter list for SQL queries
318///
319/// This macro provides a convenient way to create parameter lists without
320/// manually wrapping each value in `Value::from()`.
321///
322/// # Examples
323///
324/// ```ignore
325/// use radixdb::{Database, params};
326///
327/// let db = Database::open("memory://")?;
328/// db.execute("CREATE TABLE users (id INTEGER, name TEXT, age INTEGER)", ())?;
329///
330/// // Insert with params
331/// db.execute(
332///     "INSERT INTO users VALUES ($1, $2, $3)",
333///     params![1, "Alice", 30]
334/// )?;
335///
336/// // Query with params
337/// let rows = db.query(
338///     "SELECT * FROM users WHERE age > $1",
339///     params![25]
340/// )?;
341///
342/// // Mixed types work seamlessly
343/// db.execute(
344///     "INSERT INTO users VALUES ($1, $2, $3)",
345///     params![2, String::from("Bob"), 25]
346/// )?;
347/// ```
348#[macro_export]
349macro_rules! params {
350    () => {
351        $crate::ParamVec::new()
352    };
353    ($($param:expr),+ $(,)?) => {
354        {
355            let mut params = $crate::ParamVec::new();
356            $(params.push($crate::ToParam::to_param(&$param));)+
357            params
358        }
359    };
360}
361
362/// Named parameters for SQL queries
363///
364/// This struct holds named parameter bindings that can be used with
365/// the `:name` syntax in SQL queries.
366///
367/// # Examples
368///
369/// ```ignore
370/// use radixdb::{Database, NamedParams, named_params};
371///
372/// let db = Database::open("memory://")?;
373/// db.execute("CREATE TABLE users (id INTEGER, name TEXT)", ())?;
374///
375/// // Using the named_params! macro
376/// db.execute_named(
377///     "INSERT INTO users VALUES (:id, :name)",
378///     named_params!{ id: 1, name: "Alice" }
379/// )?;
380///
381/// // Building NamedParams manually
382/// let params = NamedParams::new()
383///     .add("id", 2)
384///     .add("name", "Bob");
385/// db.execute_named("INSERT INTO users VALUES (:id, :name)", params)?;
386/// ```
387#[derive(Debug, Clone, Default)]
388pub struct NamedParams {
389    params: FxHashMap<String, Value>,
390}
391
392impl NamedParams {
393    /// Create empty named params
394    pub fn new() -> Self {
395        Self {
396            params: FxHashMap::default(),
397        }
398    }
399
400    /// Create empty named params with pre-allocated capacity
401    pub fn with_capacity(capacity: usize) -> Self {
402        Self {
403            params: FxHashMap::with_capacity_and_hasher(capacity, Default::default()),
404        }
405    }
406
407    /// Add a named parameter (builder style)
408    pub fn add<T: ToParam>(mut self, name: impl Into<String>, value: T) -> Self {
409        self.params.insert(name.into(), value.to_param());
410        self
411    }
412
413    /// Insert a named parameter
414    pub fn insert<T: ToParam>(&mut self, name: impl Into<String>, value: T) {
415        self.params.insert(name.into(), value.to_param());
416    }
417
418    /// Get the underlying FxHashMap
419    pub fn into_inner(self) -> FxHashMap<String, Value> {
420        self.params
421    }
422
423    /// Get a reference to the underlying FxHashMap
424    pub fn as_map(&self) -> &FxHashMap<String, Value> {
425        &self.params
426    }
427}
428
429impl From<HashMap<String, Value>> for NamedParams {
430    fn from(params: HashMap<String, Value>) -> Self {
431        Self {
432            params: params.into_iter().collect(),
433        }
434    }
435}
436
437/// Create named parameters for SQL queries
438///
439/// This macro provides a convenient way to create named parameter bindings
440/// for use with the `:name` syntax in SQL queries.
441///
442/// # Examples
443///
444/// ```ignore
445/// use radixdb::{Database, named_params};
446///
447/// let db = Database::open("memory://")?;
448/// db.execute("CREATE TABLE users (id INTEGER, name TEXT, active BOOLEAN)", ())?;
449///
450/// // Insert with named params
451/// db.execute_named(
452///     "INSERT INTO users VALUES (:id, :name, :active)",
453///     named_params!{ id: 1, name: "Alice", active: true }
454/// )?;
455///
456/// // Query with named params
457/// let rows = db.query_named(
458///     "SELECT * FROM users WHERE name = :name",
459///     named_params!{ name: "Alice" }
460/// )?;
461/// ```
462#[macro_export]
463macro_rules! named_params {
464    () => {
465        $crate::NamedParams::new()
466    };
467    ($($name:ident : $value:expr),+ $(,)?) => {
468        {
469            let mut params = $crate::NamedParams::new();
470            $(
471                params.insert(stringify!($name), $value);
472            )+
473            params
474        }
475    };
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    #[test]
483    fn test_to_param_integers() {
484        assert_eq!(42i64.to_param(), Value::Integer(42));
485        assert_eq!(42i32.to_param(), Value::Integer(42));
486        assert_eq!(42i16.to_param(), Value::Integer(42));
487        assert_eq!(42i8.to_param(), Value::Integer(42));
488        assert_eq!(42u32.to_param(), Value::Integer(42));
489        assert_eq!(42u16.to_param(), Value::Integer(42));
490        assert_eq!(42u8.to_param(), Value::Integer(42));
491    }
492
493    #[test]
494    fn test_to_param_floats() {
495        assert_eq!(3.5f64.to_param(), Value::Float(3.5));
496        assert_eq!(3.5f32.to_param(), Value::Float(3.5f32 as f64));
497    }
498
499    #[test]
500    fn test_to_param_strings() {
501        assert_eq!("hello".to_param(), Value::text("hello"));
502        assert_eq!(String::from("world").to_param(), Value::text("world"));
503    }
504
505    #[test]
506    fn test_arc_str_boundary_preserves_allocation() {
507        let arc: Arc<str> = Arc::from("a heap string longer than the inline boundary");
508        let ptr = arc.as_ptr();
509
510        let smart = SmartString::from(Arc::clone(&arc));
511        assert_eq!(smart.as_str().as_ptr(), ptr);
512        assert_eq!(Arc::strong_count(&arc), 2);
513        drop(smart);
514
515        let value = Value::text_arc(Arc::clone(&arc));
516        let Value::Text(text) = &value else {
517            panic!("text_arc must produce Value::Text");
518        };
519        assert_eq!(text.as_str().as_ptr(), ptr);
520        assert_eq!(Arc::strong_count(&arc), 2);
521        drop(value);
522
523        let param = arc.to_param();
524        let Value::Text(text) = &param else {
525            panic!("Arc<str>::to_param must produce Value::Text");
526        };
527        assert_eq!(text.as_str().as_ptr(), ptr);
528        assert_eq!(Arc::strong_count(&arc), 2);
529    }
530
531    #[test]
532    fn test_to_param_bool() {
533        assert_eq!(true.to_param(), Value::Boolean(true));
534        assert_eq!(false.to_param(), Value::Boolean(false));
535    }
536
537    #[test]
538    fn test_to_param_option() {
539        assert_eq!(Some(42i64).to_param(), Value::Integer(42));
540        assert!(Option::<i64>::None.to_param().is_null());
541    }
542
543    #[test]
544    fn test_params_empty() {
545        let params: ParamVec = ().into_params();
546        assert!(params.is_empty());
547    }
548
549    #[test]
550    fn test_params_tuple() {
551        let params = (1i64, "hello", 3.5f64).into_params();
552        assert_eq!(params.len(), 3);
553        assert_eq!(params[0], Value::Integer(1));
554        assert_eq!(params[1], Value::text("hello"));
555        assert_eq!(params[2], Value::Float(3.5));
556    }
557
558    #[test]
559    fn test_params_macro() {
560        let p = params![1, "hello", 3.5];
561        let params = p.into_params();
562        assert_eq!(params.len(), 3);
563        assert_eq!(params[0], Value::Integer(1));
564        assert_eq!(params[1], Value::text("hello"));
565        assert_eq!(params[2], Value::Float(3.5));
566    }
567
568    #[test]
569    fn test_params_macro_empty() {
570        let p = params![];
571        let params: ParamVec = p.into_params();
572        assert!(params.is_empty());
573    }
574
575    #[test]
576    fn test_params_with_option() {
577        let name: Option<&str> = Some("Alice");
578        let age: Option<i32> = None;
579        let params = (1i64, name, age).into_params();
580
581        assert_eq!(params.len(), 3);
582        assert_eq!(params[0], Value::Integer(1));
583        assert_eq!(params[1], Value::text("Alice"));
584        assert!(params[2].is_null());
585    }
586
587    #[test]
588    fn test_params_from_param_vec() {
589        // ParamVec -> ParamVec should be zero-cost identity
590        let mut pv = ParamVec::new();
591        pv.push(Value::Integer(1));
592        pv.push(Value::text("hello"));
593        pv.push(Value::Float(3.5));
594
595        let result = pv.into_params();
596        assert_eq!(result.len(), 3);
597        assert_eq!(result[0], Value::Integer(1));
598        assert_eq!(result[1], Value::text("hello"));
599        assert_eq!(result[2], Value::Float(3.5));
600    }
601
602    #[test]
603    fn test_params_from_empty_param_vec() {
604        let pv = ParamVec::new();
605        let result = pv.into_params();
606        assert!(result.is_empty());
607    }
608}